, emit: () => void, value: T): T {
+ if (!isPromise(value)) {
+ return value
+ }
+ if (cache.abortController) {
+ cache.abortController.abort()
+ }
+
+ const { promise, controller } = cancelablePromise(value, cache.abortController)
+ cache.abortController = controller
+
+ return promise
+ .then((result) => {
+ cache.current = result as T
+ emit()
+ })
+ .catch((error) => {
+ if (isAbortError(error)) {
+ return
+ }
+ cache.current = error as T
+ emit()
+ }) as T
+}
diff --git a/packages/core/utils/create-emitter.ts b/packages/core/utils/create-emitter.ts
new file mode 100644
index 0000000..c3bbb4c
--- /dev/null
+++ b/packages/core/utils/create-emitter.ts
@@ -0,0 +1,55 @@
+export type EmitterSubscribe = (listener: (...params: P[]) => void) => () => void
+export interface Emitter {
+ subscribe: EmitterSubscribe
+ subscribeToOtherEmitter: (emitter: Emitter) => void
+ getSnapshot: () => T
+ getInitialSnapshot?: () => T
+ emit: (...params: P[]) => void
+ getSize: () => number
+ clear: () => void
+ contains: (listener: (...params: P[]) => void) => boolean
+}
+
+/**
+ * Generics parameters are:
+ * T: Type of the state
+ * R: Type of the snapshot
+ * P: Type of the parameters
+ * @param getSnapshot
+ * @returns
+ */
+export function createEmitter(getSnapshot: () => T, getInitialSnapshot?: () => T): Emitter {
+ const listeners = new Set<(...params: P[]) => void>()
+ // const listeners = new WeakSet<(...params: P[]) => void>()
+ const otherCleaners: Array<() => void> = []
+ return {
+ clear: () => {
+ for (const cleaner of otherCleaners) {
+ cleaner()
+ }
+
+ listeners.clear()
+ },
+ subscribe: (listener) => {
+ listeners.add(listener)
+ return () => {
+ listeners.delete(listener)
+ }
+ },
+ emit: (...params) => {
+ for (const listener of listeners) {
+ listener(...params)
+ }
+ },
+ contains: (listener) => listeners.has(listener),
+ getSnapshot,
+ getInitialSnapshot,
+ getSize: () => listeners.size,
+ subscribeToOtherEmitter(emitter) {
+ const clean = emitter.subscribe(() => {
+ this.emit()
+ })
+ otherCleaners.push(clean)
+ },
+ }
+}
diff --git a/src/is.ts b/packages/core/utils/is.ts
similarity index 57%
rename from src/is.ts
rename to packages/core/utils/is.ts
index 1b7a30d..7f28229 100644
--- a/src/is.ts
+++ b/packages/core/utils/is.ts
@@ -1,21 +1,13 @@
+import type { SetStateCb, SetValue, State } from '../types'
import { Abort } from './common'
-import type { Ref, Setter, SetValue } from './types'
-export function isPromise(value: unknown): value is Promise {
+export function isPromise(value: unknown): value is Promise {
return value instanceof Promise
}
-export function isFunction(value: unknown): value is (...args: unknown[]) => unknown {
- return typeof value === 'function'
-}
-export function isSetValueFunction(value: SetValue): value is Setter {
+
+export function isFunction unknown>(value: unknown): value is T {
return typeof value === 'function'
}
-export function isObject(value: unknown): value is Record {
- return typeof value === 'object' && value !== null
-}
-export function isRef(value: unknown): value is Ref {
- return isObject(value) && value.isRef === true
-}
export function isMap(value: unknown): value is Map {
return value instanceof Map
@@ -35,11 +27,21 @@ export function isEqualBase(valueA: T, valueB: T): boolean {
}
return !!Object.is(valueA, valueB)
}
-
+export function isSetValueFunction(value: SetValue): value is SetStateCb {
+ return typeof value === 'function'
+}
export function isAbortError(value: unknown): value is DOMException {
return value instanceof DOMException && value.name === Abort.Error
}
-export function isAnyOtherError(value: unknown): value is Error {
- return value instanceof Error && value.name !== Abort.Error
+export function isError(value: unknown): value is Error {
+ return value instanceof Error
+}
+
+export function isUndefined(value: unknown): value is undefined {
+ return value === undefined
+}
+
+export function isState(value: unknown): value is State {
+ return isFunction(value) && 'get' in value && 'set' in value && 'isSet' in value && value.isSet === true
}
diff --git a/src/shallow.ts b/packages/core/utils/shallow.ts
similarity index 87%
rename from src/shallow.ts
rename to packages/core/utils/shallow.ts
index 48af059..bf39dec 100644
--- a/src/shallow.ts
+++ b/packages/core/utils/shallow.ts
@@ -1,6 +1,6 @@
+/* eslint-disable sonarjs/cognitive-complexity */
import { isArray, isMap, isSet } from './is'
-// eslint-disable-next-line sonarjs/cognitive-complexity
export function shallow(valueA: T, valueB: T): boolean {
if (valueA == valueB) {
return true
@@ -40,8 +40,8 @@ export function shallow(valueA: T, valueB: T): boolean {
return true
}
- const keysA = Object.keys(valueA as Record)
- const keysB = Object.keys(valueB as Record)
+ const keysA = Object.keys(valueA)
+ const keysB = Object.keys(valueB)
if (keysA.length !== keysB.length) return false
for (const key of keysA) {
if (
diff --git a/packages/examples/vite-project/.gitignore b/packages/examples/vite-project/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/packages/examples/vite-project/.gitignore
@@ -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?
diff --git a/packages/examples/vite-project/README.md b/packages/examples/vite-project/README.md
new file mode 100644
index 0000000..74872fd
--- /dev/null
+++ b/packages/examples/vite-project/README.md
@@ -0,0 +1,50 @@
+# React + TypeScript + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
+
+- Configure the top-level `parserOptions` property like this:
+
+```js
+export default tseslint.config({
+ languageOptions: {
+ // other options...
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ },
+})
+```
+
+- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
+- Optionally add `...tseslint.configs.stylisticTypeChecked`
+- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
+
+```js
+// eslint.config.js
+import react from 'eslint-plugin-react'
+
+export default tseslint.config({
+ // Set the react version
+ settings: { react: { version: '18.3' } },
+ plugins: {
+ // Add the react plugin
+ react,
+ },
+ rules: {
+ // other rules...
+ // Enable its recommended rules
+ ...react.configs.recommended.rules,
+ ...react.configs['jsx-runtime'].rules,
+ },
+})
+```
diff --git a/packages/examples/vite-project/bun.lockb b/packages/examples/vite-project/bun.lockb
new file mode 100755
index 0000000..48a5bf2
Binary files /dev/null and b/packages/examples/vite-project/bun.lockb differ
diff --git a/packages/examples/vite-project/index.html b/packages/examples/vite-project/index.html
new file mode 100644
index 0000000..e4b78ea
--- /dev/null
+++ b/packages/examples/vite-project/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Vite + React + TS
+
+
+
+
+
+
diff --git a/packages/examples/vite-project/package.json b/packages/examples/vite-project/package.json
new file mode 100644
index 0000000..2ff79bb
--- /dev/null
+++ b/packages/examples/vite-project/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "vite-project",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.13.0",
+ "@types/react": "^18.3.12",
+ "@types/react-dom": "^18.3.1",
+ "@vitejs/plugin-react-swc": "^3.5.0",
+ "eslint": "^9.13.0",
+ "eslint-plugin-react-hooks": "^5.0.0",
+ "eslint-plugin-react-refresh": "^0.4.14",
+ "globals": "^15.11.0",
+ "typescript": "~5.6.2",
+ "typescript-eslint": "^8.11.0",
+ "vite": "^5.4.10"
+ },
+ "dependencies": {
+ "zustand": "^5.0.1"
+ }
+}
diff --git a/packages/examples/vite-project/public/vite.svg b/packages/examples/vite-project/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/packages/examples/vite-project/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/examples/vite-project/src/App.css b/packages/examples/vite-project/src/App.css
new file mode 100644
index 0000000..b9d355d
--- /dev/null
+++ b/packages/examples/vite-project/src/App.css
@@ -0,0 +1,42 @@
+#root {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
diff --git a/packages/examples/vite-project/src/App.tsx b/packages/examples/vite-project/src/App.tsx
new file mode 100644
index 0000000..59d9f8e
--- /dev/null
+++ b/packages/examples/vite-project/src/App.tsx
@@ -0,0 +1,36 @@
+import { create, select } from '../../../core'
+
+// Define atoms for the three states
+const state1Atom = create(0)
+const state2Atom = create(0)
+const state3Atom = create(0)
+
+export default function App() {
+ console.log('App render')
+ return (
+
+ state1Atom.set((c) => c + 1)}>Increment counter 1"
+ state1Atom.set((c) => c + 1)}>Increment counter 2"
+ state3Atom.set((m) => m + 1)}>Increment counter 3"
+ {
+ state1Atom.set((c) => c + 1)
+ state2Atom.set((c) => c + 1)
+ state3Atom.set((m) => m + 1)
+ }}
+ >
+ Increment All"
+
+
+
+ )
+}
+
+const sumState = select([state1Atom, state2Atom, state3Atom], (a, b, c) => a + b + c)
+
+function ComponentChild1() {
+ console.log('ComponentChild1 render')
+ // Use the state atom in the child component
+ const state1 = sumState()
+ return Sum of states: {state1}
+}
diff --git a/packages/examples/vite-project/src/assets/react.svg b/packages/examples/vite-project/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/packages/examples/vite-project/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/examples/vite-project/src/index.css b/packages/examples/vite-project/src/index.css
new file mode 100644
index 0000000..6119ad9
--- /dev/null
+++ b/packages/examples/vite-project/src/index.css
@@ -0,0 +1,68 @@
+:root {
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+}
+a:hover {
+ color: #535bf2;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+}
+button:hover {
+ border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+ a:hover {
+ color: #747bff;
+ }
+ button {
+ background-color: #f9f9f9;
+ }
+}
diff --git a/packages/examples/vite-project/src/jotai-example.tsx b/packages/examples/vite-project/src/jotai-example.tsx
new file mode 100644
index 0000000..c197d4c
--- /dev/null
+++ b/packages/examples/vite-project/src/jotai-example.tsx
@@ -0,0 +1,33 @@
+import { atomFamily } from 'jotai/utils'
+import { atom, useAtom, useSetAtom } from 'jotai'
+import React, { useState } from 'react'
+
+// Define atoms for the counters
+const counter1Atom = atom(0)
+const counter2Atom = atom(0)
+
+// Use `atomFamily` to create parameterized atoms
+const derivedSumAtomFamily = atomFamily((multiplier: number) =>
+ atom((get) => {
+ const counter1 = get(counter1Atom)
+ const counter2 = get(counter2Atom)
+ return (counter1 + counter2) * multiplier
+ }),
+)
+
+export default function App() {
+ const [counter1, setCounter1] = useAtom(counter1Atom)
+ const [counter2, setCounter2] = useAtom(counter2Atom)
+ const [multiplier, setMultiplier] = useState(1)
+
+ // Use the derived atom from the atomFamily with the multiplier
+ const [derivedSum] = useAtom(derivedSumAtomFamily(multiplier))
+ return (
+
+ setCounter1((c) => c + 1)}>Increment counter 1 "value: {counter1}"
+ setCounter2((c) => c + 1)}>Increment counter 2 "value: {counter2}"
+ setMultiplier((m) => m + 1)}>Increment multiplier "value: {multiplier}"
+ Result of multiplier: {derivedSum}
+
+ )
+}
diff --git a/packages/examples/vite-project/src/main.tsx b/packages/examples/vite-project/src/main.tsx
new file mode 100644
index 0000000..5fb749d
--- /dev/null
+++ b/packages/examples/vite-project/src/main.tsx
@@ -0,0 +1,6 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.tsx'
+
+createRoot(document.getElementById('root')!).render( )
diff --git a/packages/examples/vite-project/src/muya-example.tsx b/packages/examples/vite-project/src/muya-example.tsx
new file mode 100644
index 0000000..c197d4c
--- /dev/null
+++ b/packages/examples/vite-project/src/muya-example.tsx
@@ -0,0 +1,33 @@
+import { atomFamily } from 'jotai/utils'
+import { atom, useAtom, useSetAtom } from 'jotai'
+import React, { useState } from 'react'
+
+// Define atoms for the counters
+const counter1Atom = atom(0)
+const counter2Atom = atom(0)
+
+// Use `atomFamily` to create parameterized atoms
+const derivedSumAtomFamily = atomFamily((multiplier: number) =>
+ atom((get) => {
+ const counter1 = get(counter1Atom)
+ const counter2 = get(counter2Atom)
+ return (counter1 + counter2) * multiplier
+ }),
+)
+
+export default function App() {
+ const [counter1, setCounter1] = useAtom(counter1Atom)
+ const [counter2, setCounter2] = useAtom(counter2Atom)
+ const [multiplier, setMultiplier] = useState(1)
+
+ // Use the derived atom from the atomFamily with the multiplier
+ const [derivedSum] = useAtom(derivedSumAtomFamily(multiplier))
+ return (
+
+ setCounter1((c) => c + 1)}>Increment counter 1 "value: {counter1}"
+ setCounter2((c) => c + 1)}>Increment counter 2 "value: {counter2}"
+ setMultiplier((m) => m + 1)}>Increment multiplier "value: {multiplier}"
+ Result of multiplier: {derivedSum}
+
+ )
+}
diff --git a/packages/examples/vite-project/src/vite-env.d.ts b/packages/examples/vite-project/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/packages/examples/vite-project/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/packages/examples/vite-project/tsconfig.app.json b/packages/examples/vite-project/tsconfig.app.json
new file mode 100644
index 0000000..f867de0
--- /dev/null
+++ b/packages/examples/vite-project/tsconfig.app.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "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-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/packages/examples/vite-project/tsconfig.json b/packages/examples/vite-project/tsconfig.json
new file mode 100644
index 0000000..d32ff68
--- /dev/null
+++ b/packages/examples/vite-project/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "files": [],
+ "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
+}
diff --git a/packages/examples/vite-project/tsconfig.node.json b/packages/examples/vite-project/tsconfig.node.json
new file mode 100644
index 0000000..abcd7f0
--- /dev/null
+++ b/packages/examples/vite-project/tsconfig.node.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2022",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "Bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/packages/examples/vite-project/vite.config.ts b/packages/examples/vite-project/vite.config.ts
new file mode 100644
index 0000000..2328e17
--- /dev/null
+++ b/packages/examples/vite-project/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react-swc'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+})
diff --git a/src/__tests__/common.test.ts b/src/__tests__/common.test.ts
deleted file mode 100644
index effa010..0000000
--- a/src/__tests__/common.test.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { cancelablePromise, toType, useSyncExternalStore } from '../common'
-import { renderHook } from '@testing-library/react-hooks'
-import { createEmitter } from '../create-emitter'
-import { longPromise } from './test-utils'
-
-describe('toType', () => {
- it('should cast object to specified type', () => {
- const object = { a: 1 }
- const result = toType<{ a: number }>(object)
- expect(result.a).toBe(1)
- })
-})
-
-describe('useSyncExternalStore', () => {
- it('should return the initial state value', () => {
- const emitter = createEmitter(() => 0)
- const { result } = renderHook(() => useSyncExternalStore(emitter, (state) => state))
- expect(result.current).toBe(0)
- })
-
- it('should update when the state value changes', () => {
- let value = 0
- const emitter = createEmitter(() => value)
- const { result } = renderHook(() => useSyncExternalStore(emitter, (state) => state))
-
- value = 1
- emitter.emit()
- expect(result.current).toBe(1)
- })
-
- it('should use the selector function', () => {
- let value = 0
- const emitter = createEmitter(() => ({ count: value }))
- const { result } = renderHook(() => useSyncExternalStore(emitter, (state: { count: number }) => state.count))
-
- value = 1
- emitter.emit()
- expect(result.current).toBe(1)
- })
-
- it('should use the isEqual function', () => {
- let value = 0
- const emitter = createEmitter(() => ({ count: value }))
- const isEqual = jest.fn((a, b) => a === b)
- const { result } = renderHook(() => useSyncExternalStore(emitter, (state: { count: number }) => state.count, isEqual))
-
- value = 1
- emitter.emit()
- expect(result.current).toBe(1)
- expect(isEqual).toHaveBeenCalled()
- })
-
- it('should test cancelable promise to abort', async () => {
- const { promise, controller } = cancelablePromise(longPromise(1000 * 1000))
- controller.abort()
- expect(promise).rejects.toThrow('aborted')
- })
-
- it('should test cancelable promise to resolve', async () => {
- const { promise } = cancelablePromise(longPromise(0))
- expect(await promise).toBe(0)
- })
-})
diff --git a/src/__tests__/create.test.tsx b/src/__tests__/create.test.tsx
deleted file mode 100644
index d1a5e18..0000000
--- a/src/__tests__/create.test.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import { Suspense } from 'react'
-import { create } from '../create'
-import { renderHook, waitFor, act, render } from '@testing-library/react'
-import { ErrorBoundary, longPromise } from './test-utils'
-
-describe('create', () => {
- it('should create test with base value', () => {
- const state = create(0)
- const result = renderHook(() => state())
- expect(result.result.current).toBe(0)
- })
- it('should create test with function', () => {
- const state = create(() => 0)
- const result = renderHook(() => state())
- expect(result.result.current).toBe(0)
- })
- it('should create test with promise', async () => {
- const state = create(Promise.resolve(0))
- const result = renderHook(() => state())
- await waitFor(() => {
- expect(result.result.current).toBe(0)
- })
- })
- it('should create test with promise and wait to be resolved', async () => {
- const state = create(longPromise)
- const result = renderHook(() => state(), { wrapper: ({ children }) => {children} })
-
- await waitFor(() => {
- expect(result.result.current).toBe(0)
- })
- })
- it('should create test with lazy promise and wait to be resolved', async () => {
- const state = create(async () => await longPromise())
- const result = renderHook(() => state(), { wrapper: ({ children }) => {children} })
-
- await waitFor(() => {
- expect(result.result.current).toBe(0)
- })
- })
- it('should create test with promise and set value during the promise is pending', async () => {
- const state = create(longPromise)
- const result = renderHook(() => state(), { wrapper: ({ children }) => {children} })
-
- act(() => {
- state.setState(10)
- })
- await waitFor(() => {
- expect(result.result.current).toBe(10)
- })
- })
-
- it('should create test with lazy promise and set value during the promise is pending', async () => {
- const state = create(async () => await longPromise())
- const result = renderHook(() => state(), { wrapper: ({ children }) => {children} })
-
- act(() => {
- state.setState(10)
- })
- await waitFor(() => {
- expect(result.result.current).toBe(10)
- })
- })
-
- it('should fail inside the hook when the promise is rejected with not abort isWithError', async () => {
- const state = create(Promise.reject('error-message'))
-
- function Component() {
- state()
- return null
- }
-
- const result = render(
- An error occurred.}>
-
-
-
- ,
- )
-
- await waitFor(() => {
- expect(result.container.textContent).toBe('An error occurred.')
- })
- })
-})
diff --git a/src/__tests__/is.test.ts b/src/__tests__/is.test.ts
deleted file mode 100644
index 2ecb74d..0000000
--- a/src/__tests__/is.test.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-import { Abort } from '../common'
-import {
- isPromise,
- isFunction,
- isSetValueFunction,
- isObject,
- isRef,
- isMap,
- isSet,
- isArray,
- isEqualBase,
- isAbortError,
-} from '../is'
-
-describe('isPromise', () => {
- it('should return true for a Promise', () => {
- expect(isPromise(Promise.resolve())).toBe(true)
- })
- it('should return false for a non-Promise', () => {
- expect(isPromise(123)).toBe(false)
- })
-})
-
-describe('isFunction', () => {
- it('should return true for a function', () => {
- expect(isFunction(() => {})).toBe(true)
- })
- it('should return false for a non-function', () => {
- expect(isFunction(123)).toBe(false)
- })
-})
-
-describe('isSetValueFunction', () => {
- it('should return true for a function', () => {
- expect(isSetValueFunction(() => {})).toBe(true)
- })
- it('should return false for a non-function', () => {
- expect(isSetValueFunction(123)).toBe(false)
- })
-})
-
-describe('isObject', () => {
- it('should return true for an object', () => {
- expect(isObject({})).toBe(true)
- })
- it('should return false for a non-object', () => {
- expect(isObject(123)).toBe(false)
- })
-})
-
-describe('isRef', () => {
- it('should return true for a ref object', () => {
- expect(isRef({ isRef: true })).toBe(true)
- })
- it('should return false for a non-ref object', () => {
- expect(isRef({})).toBe(false)
- })
-})
-
-describe('isMap', () => {
- it('should return true for a Map', () => {
- expect(isMap(new Map())).toBe(true)
- })
- it('should return false for a non-Map', () => {
- expect(isMap(123)).toBe(false)
- })
-})
-
-describe('isSet', () => {
- it('should return true for a Set', () => {
- expect(isSet(new Set())).toBe(true)
- })
- it('should return false for a non-Set', () => {
- expect(isSet(123)).toBe(false)
- })
-})
-
-describe('isArray', () => {
- it('should return true for an array', () => {
- expect(isArray([])).toBe(true)
- })
- it('should return false for a non-array', () => {
- expect(isArray(123)).toBe(false)
- })
-})
-
-describe('isEqualBase', () => {
- it('should return true for equal values', () => {
- expect(isEqualBase(1, 1)).toBe(true)
- })
- it('should return false for non-equal values', () => {
- expect(isEqualBase(1, 2)).toBe(false)
- })
-})
-
-describe('isAbortError', () => {
- it('should return true for an AbortError', () => {
- expect(isAbortError(new DOMException('', Abort.Error))).toBe(true)
- })
- it('should return false for a non-AbortError', () => {
- expect(isAbortError(new DOMException('', 'Error'))).toBe(false)
- })
-})
diff --git a/src/__tests__/merge.test.ts b/src/__tests__/merge.test.ts
deleted file mode 100644
index e721058..0000000
--- a/src/__tests__/merge.test.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import { create } from '../create'
-import { renderHook, act } from '@testing-library/react'
-import { merge } from '../merge'
-
-describe('merge', () => {
- it('should test merge multiple-state', () => {
- const state1 = create(1)
- const state2 = create(1)
- const state3 = create(3)
-
- const useMerged = merge([state1, state2, state3], (value1, value2, value3) => {
- expect(value1).toBe(1)
- expect(value2).toBe(1)
- expect(value3).toBe(3)
- return `${value1} ${value2} ${value3}`
- })
- const result = renderHook(() => useMerged())
- expect(result.result.current).toBe('1 1 3')
- })
-
- it('should test merge multiple-state with isEqual', () => {
- const state1 = create(1)
- const state2 = create(1)
- const state3 = create(3)
-
- const useMerged = merge(
- [state1, state2, state3],
- (value1, value2, value3) => {
- expect(value1).toBe(1)
- expect(value2).toBe(1)
- expect(value3).toBe(3)
- return `${value1} ${value2} ${value3}`
- },
- (a, b) => a === b,
- )
- const result = renderHook(() => useMerged())
- expect(result.result.current).toBe('1 1 3')
- })
-
- it('should test merge multiple-state with different type', () => {
- const state1 = create(1)
- const state2 = create('1')
- const state3 = create({ value: 3 })
- const state4 = create([1, 2, 3])
-
- const useMerged = merge([state1, state2, state3, state4], (value1, value2, value3, value4) => {
- expect(value1).toBe(1)
- expect(value2).toBe('1')
- expect(value3).toStrictEqual({ value: 3 })
- expect(value4).toStrictEqual([1, 2, 3])
- return `${value1} ${value2} ${value3.value} ${value4.join(' ')}`
- })
- const result = renderHook(() => useMerged())
- expect(result.result.current).toBe('1 1 3 1 2 3')
- })
- it('should test merge with reset', () => {
- const state1 = create(1)
- const state2 = create(1)
- const state3 = create(3)
-
- const useMerged = merge([state1, state2, state3], (value1, value2, value3) => {
- return `${value1} ${value2} ${value3}`
- })
- const result = renderHook(() => useMerged())
-
- act(() => {
- state1.setState(2)
- state2.setState(2)
- state3.setState(4)
- })
- expect(result.result.current).toBe('2 2 4')
-
- act(() => {
- useMerged.reset()
- })
- expect(result.result.current).toBe('1 1 3')
- })
-})
diff --git a/src/__tests__/state.test.tsx b/src/__tests__/state.test.tsx
deleted file mode 100644
index c0701ae..0000000
--- a/src/__tests__/state.test.tsx
+++ /dev/null
@@ -1,619 +0,0 @@
-/* eslint-disable @typescript-eslint/no-shadow */
-/* eslint-disable no-shadow */
-import { Suspense } from 'react'
-import { create } from '../create'
-import { renderHook, act, waitFor, render, screen } from '@testing-library/react'
-import { shallow } from '../shallow'
-describe('state', () => {
- it('should test state', () => {
- const appState = create({ count: 0 })
- expect(appState.getState()).toEqual({ count: 0 })
- })
-
- it('should render state with promise hook', async () => {
- const promise = Promise.resolve({ count: 100 })
- const appState = create(promise)
- const renderCount = { current: 0 }
- // const
-
- const result = renderHook(() => {
- renderCount.current++
- return appState()
- })
- // wait for the promise to be resolved
- await waitFor(() => {})
- expect(result.result.current).toEqual({ count: 100 })
- // count rendered
- expect(renderCount.current).toEqual(2)
- expect(appState.getState()).toEqual({ count: 100 })
- })
-
- it('should render state with get promise hook', async () => {
- // eslint-disable-next-line unicorn/consistent-function-scoping
- const getPromise = () => Promise.resolve({ count: 100 })
- const appState = create(getPromise)
- const renderCount = { current: 0 }
- // const
-
- const result = renderHook(() => {
- renderCount.current++
- return appState()
- })
- // wait for the promise to be resolved
- await waitFor(() => {})
- act(() => {
- appState.setState({ count: 15 })
- })
- expect(result.result.current).toEqual({ count: 15 })
- // count rendered
- expect(renderCount.current).toEqual(3)
- expect(appState.getState()).toEqual({ count: 15 })
- })
-
- it('should render state with get promise check default', async () => {
- // eslint-disable-next-line unicorn/consistent-function-scoping
- const getPromise = () => Promise.resolve({ count: 100 })
- const appState = create(getPromise)
- // const
-
- // wait for the promise to be resolved
- await waitFor(() => {})
- act(() => {
- appState.setState({ count: 15 })
- })
- expect(appState.getState()).toEqual({ count: 15 })
- // count rendered
- act(() => {
- appState.reset()
- })
- expect(appState.getState()).toEqual({ count: 15 })
- })
-
- it('should render state with get hook', async () => {
- // eslint-disable-next-line unicorn/consistent-function-scoping
- const get = () => ({ count: 100 })
- const appState = create(get)
- const renderCount = { current: 0 }
- // const
-
- const result = renderHook(() => {
- renderCount.current++
- return appState()
- })
- // wait for the promise to be resolved
- await waitFor(() => {})
- act(() => {
- appState.setState({ count: 15 })
- })
- expect(result.result.current).toEqual({ count: 15 })
- // count rendered
- expect(renderCount.current).toEqual(2)
- expect(appState.getState()).toEqual({ count: 15 })
-
- act(() => {
- appState.reset()
- })
- expect(result.result.current).toEqual({ count: 100 })
- })
-
- it('should render state with get', async () => {
- let wasCalled = false
- const get = () => {
- wasCalled = true
- return { count: 100 }
- }
- const appState = create(get)
- expect(wasCalled).toEqual(false)
- appState.getState()
- expect(wasCalled).toEqual(true)
- })
- it('should render state with get hook', async () => {
- let wasCalled = false
- const get = () => {
- wasCalled = true
- return { count: 100 }
- }
- const appState = create(get)
- expect(wasCalled).toEqual(false)
- renderHook(() => {
- appState()
- })
- expect(wasCalled).toEqual(true)
- })
-
- it('should render state with promise with suspense', async () => {
- const promise = Promise.resolve({ count: 100 })
- const appState = create(promise)
- const renderCount = { current: 0 }
-
- const MockedComponent = jest.fn(() => loading
)
- const MockedComponentAfterSuspense = jest.fn(() => loaded
)
- // const
- function Component() {
- renderCount.current++
- return (
-
- {appState().count}
-
-
- )
- }
- render(
- }>
-
- ,
- )
- expect(MockedComponent).toHaveBeenCalledTimes(1)
- expect(MockedComponentAfterSuspense).toHaveBeenCalledTimes(0)
- await waitFor(() => {
- return screen.getByText('100')
- })
- expect(MockedComponent).toHaveBeenCalledTimes(1)
- expect(MockedComponentAfterSuspense).toHaveBeenCalledTimes(1)
- })
-
- it('should render state', () => {
- const appState = create({ count: 0 })
- const renderCount = { current: 0 }
- // const
-
- const result = renderHook(() => {
- renderCount.current++
- return appState()
- })
- expect(result.result.current).toEqual({ count: 0 })
- // count rendered
- expect(renderCount.current).toEqual(1)
- })
-
- it('should render state', () => {
- const appState = create({ count: 0 })
- const slice = appState.select((slice) => slice.count)
- const renderCount = { current: 0 }
- // const
-
- const result = renderHook(() => {
- renderCount.current++
- return slice()
- })
- expect(result.result.current).toEqual(0)
- // count rendered
- expect(renderCount.current).toEqual(1)
- })
-
- it('should render state with change', () => {
- const appState = create({ count: 0 })
- const renderCount = { current: 0 }
- // const
-
- const result = renderHook(() => {
- renderCount.current++
- return appState((slice) => slice)
- })
-
- act(() => {
- appState.setState({ count: 1 })
- })
- expect(result.result.current).toEqual({ count: 1 })
- expect(renderCount.current).toEqual(2)
- })
-
- it('should render state with slice change', () => {
- const appState = create({ count: { nested: 0, array: [0] } })
- const renderCount = { current: 0 }
- const useNestedSlice = appState.select((slice) => slice.count)
- const useNestedSliceArray = appState.select((slice) => slice.count.array.length)
- const result = renderHook(() => {
- return appState()
- })
- const sliceResult = renderHook(() => {
- renderCount.current++
- return useNestedSlice()
- })
- const sliceArrayResult = renderHook(() => {
- return useNestedSliceArray()
- })
- expect(sliceArrayResult.result.current).toEqual(1)
- expect(sliceResult.result.current).toEqual({ nested: 0, array: [0] })
- act(() => {
- appState.setState({ count: { nested: 2, array: [0] } })
- })
-
- expect(result.result.current).toEqual({ count: { nested: 2, array: [0] } })
- expect(sliceResult.result.current).toEqual({ nested: 2, array: [0] })
-
- act(() => {
- appState.setState({ count: { nested: 2, array: [1, 2, 4] } })
- })
- expect(sliceArrayResult.result.current).toEqual(3)
- })
-
- it('should render multiple state', () => {
- const mainState = create({ count: { nestedCount: 2 } })
- const slice1 = mainState.select((slice) => slice.count)
- const slice2FromSlice1 = slice1.select((slice) => slice.nestedCount)
-
- const slice2FromSlice1Result = renderHook(() => slice2FromSlice1())
- expect(slice2FromSlice1Result.result.current).toEqual(2)
-
- act(() => {
- mainState.setState({ count: { nestedCount: 3 } })
- })
- expect(slice2FromSlice1Result.result.current).toEqual(3)
- })
-
- it('should render multiple state with change', () => {
- const appState = create({ count: 0 })
- const renderCount1 = { current: 0 }
- const renderCount2 = { current: 0 }
- // const
-
- const result1 = renderHook(() => {
- renderCount1.current++
- return appState()
- })
- const result2 = renderHook(() => {
- renderCount2.current++
- return appState((slice) => slice.count)
- })
- act(() => {
- appState.setState({ count: 1 })
- })
- expect(result1.result.current).toEqual({ count: 1 })
- expect(result2.result.current).toEqual(1)
- expect(renderCount1.current).toEqual(2)
- expect(renderCount2.current).toEqual(2)
- })
-
- it('should test initial state', () => {
- const appState = create({ count: 0 })
- expect(appState.getState()).toEqual({ count: 0 })
- })
-
- it('should render initial state', () => {
- const appState = create({ count: 0 })
- const renderCount = { current: 0 }
-
- const result = renderHook(() => {
- renderCount.current++
- return appState()
- })
- expect(result.result.current).toEqual({ count: 0 })
- expect(renderCount.current).toEqual(1)
- })
-
- it('should render state after change', () => {
- const appState = create({ count: 0 })
- const renderCount = { current: 0 }
-
- const result = renderHook(() => {
- renderCount.current++
- return appState((slice) => slice)
- })
-
- act(() => {
- appState.setState({ count: 1 })
- })
- expect(result.result.current).toEqual({ count: 1 })
- expect(renderCount.current).toEqual(2)
- })
-
- it('should render state with nested slice change', () => {
- const appState = create({ count: { nested: 0, array: [0] } })
- const renderCount = { current: 0 }
- const useNestedSlice = appState.select((slice) => slice.count)
- const useNestedSliceArray = appState.select((slice) => slice.count.array.length)
-
- const result = renderHook(() => appState())
- const sliceResult = renderHook(() => {
- renderCount.current++
- return useNestedSlice()
- })
- const sliceArrayResult = renderHook(() => useNestedSliceArray())
-
- expect(sliceArrayResult.result.current).toEqual(1)
- expect(sliceResult.result.current).toEqual({ nested: 0, array: [0] })
-
- act(() => {
- appState.setState({ count: { nested: 2, array: [0] } })
- })
- expect(result.result.current).toEqual({ count: { nested: 2, array: [0] } })
- expect(sliceResult.result.current).toEqual({ nested: 2, array: [0] })
-
- act(() => {
- appState.setState({ count: { nested: 2, array: [1, 2, 4] } })
- })
- expect(sliceArrayResult.result.current).toEqual(3)
- })
-
- it('should render multiple state slices with updates', () => {
- const mainState = create({ count: { nestedCount: 2 } })
- const slice1 = mainState.select((slice) => slice.count)
- const slice2FromSlice1 = slice1.select((slice) => slice.nestedCount)
-
- const slice2FromSlice1Result = renderHook(() => slice2FromSlice1())
- expect(slice2FromSlice1Result.result.current).toEqual(2)
-
- act(() => {
- mainState.setState({ count: { nestedCount: 3 } })
- })
- expect(slice2FromSlice1Result.result.current).toEqual(3)
- })
-
- it('should render multiple components observing the same state', () => {
- const appState = create({ count: 0 })
- const renderCount1 = { current: 0 }
- const renderCount2 = { current: 0 }
-
- const result1 = renderHook(() => {
- renderCount1.current++
- return appState()
- })
- const result2 = renderHook(() => {
- renderCount2.current++
- return appState((slice) => slice.count)
- })
-
- act(() => {
- appState.setState({ count: 1 })
- })
- expect(result1.result.current).toEqual({ count: 1 })
- expect(result2.result.current).toEqual(1)
- expect(renderCount1.current).toEqual(2)
- expect(renderCount2.current).toEqual(2)
- })
-
- it('should reset state to default value', () => {
- const appState = create({ count: 0 })
- act(() => {
- appState.setState({ count: 10 })
- })
- expect(appState.getState()).toEqual({ count: 10 })
-
- act(() => {
- appState.reset()
- })
- expect(appState.getState()).toEqual({ count: 0 })
- })
-
- it('should handle updates with deep nesting in state', () => {
- const appState = create({ data: { nested: { value: 1 } } })
- const nestedSlice = appState.select((s) => s.data.nested.value)
-
- const result = renderHook(() => nestedSlice())
- expect(result.result.current).toEqual(1)
-
- act(() => {
- appState.setState({ data: { nested: { value: 2 } } })
- })
- expect(result.result.current).toEqual(2)
- })
-
- it('should not re-render for unrelated slice changes', () => {
- const appState = create({ count: 0, unrelated: 5 })
- const renderCount = { current: 0 }
-
- const countSlice = appState.select((state) => state.count)
- const unrelatedSlice = appState.select((state) => state.unrelated)
-
- renderHook(() => {
- renderCount.current++
- return countSlice()
- })
-
- const unrelatedResult = renderHook(() => unrelatedSlice())
- expect(renderCount.current).toEqual(1)
-
- act(() => {
- return appState.setState({ unrelated: 10 } as never)
- })
-
- expect(unrelatedResult.result.current).toEqual(10)
- expect(renderCount.current).toEqual(2) // No re-render for count slice
- })
-
- it('should not re-render where isEqual return true on state', () => {
- const appState = create({ count: 0 }, () => true)
- const renderCount = { current: 0 }
-
- renderHook(() => {
- renderCount.current++
- return appState()
- })
-
- act(() => {
- appState.setState({ count: 10 })
- })
-
- expect(renderCount.current).toEqual(1)
- })
-
- it('should not re-render where isEqual return true hook slice', () => {
- const appState = create({ count: 0 })
- const renderCount = { current: 0 }
-
- renderHook(() => {
- renderCount.current++
- return appState(
- (slice) => slice,
- () => true,
- )
- })
-
- act(() => {
- appState.setState({ count: 10 })
- })
-
- expect(renderCount.current).toEqual(1)
- })
-
- it('should not re-render where isEqual return true on slice', () => {
- const appState = create({ count: 0 })
- const appStateSlice = appState.select(
- (slice) => slice.count,
- () => true,
- )
- const renderCount = { current: 0 }
-
- renderHook(() => {
- renderCount.current++
- return appStateSlice()
- })
-
- act(() => {
- appState.setState({ count: 10 })
- })
- expect(renderCount.current).toEqual(1)
- })
-
- it('should not re-render where isEqual return true on nested slice', () => {
- const appState = create({ count: { nested: { count: 0 } } })
- const appStateSlice = appState.select((slice) => slice.count)
- const nestedAppSlice = appStateSlice.select(
- (slice) => slice.nested.count,
- () => true,
- )
- const renderCount = { current: 0 }
-
- renderHook(() => {
- renderCount.current++
- return nestedAppSlice()
- })
-
- act(() => {
- appState.setState({ count: { nested: { count: 10 } } })
- })
- expect(renderCount.current).toEqual(1)
- })
-
- it('should use slice with new reference', () => {
- const useName = create(() => 'John')
- const useDifferentName = useName.select(
- (name) => ({
- name,
- }),
- shallow,
- )
- const result = renderHook(() => useDifferentName())
- expect(result.result.current).toEqual({ name: 'John' })
- act(() => {
- useName.setState('Jane')
- })
- expect(result.result.current).toEqual({ name: 'Jane' })
- })
- it('should check if subscribe works', () => {
- const appState = create({ count: 0 })
- let count = 0
- const unsubscribe = appState.subscribe((state) => {
- expect(state).toEqual({ count })
- count++
- })
-
- act(() => {
- appState.setState({ count: 1 })
- })
- expect(count).toEqual(2)
-
- unsubscribe()
- act(() => {
- appState.setState({ count: 2 })
- })
- expect(count).toEqual(2)
- })
-
- it('should handle rapid consecutive state updates', () => {
- const appState = create({ count: 0 })
- const renderCount = { current: 0 }
-
- const result = renderHook(() => {
- renderCount.current++
- return appState()
- })
-
- act(() => {
- // batch updates
- appState.setState({ count: 1 })
- appState.setState({ count: 2 })
- appState.setState({ count: 3 })
- })
-
- expect(result.result.current).toEqual({ count: 3 })
- expect(renderCount.current).toEqual(2) // it's batch
- })
-
- it('should handle setting state to the same value', () => {
- const appState = create({ count: 0 })
- const renderCount = { current: 0 }
-
- const result = renderHook(() => {
- renderCount.current++
- return appState()
- })
-
- act(() => {
- appState.setState((previous) => previous)
- })
-
- expect(result.result.current).toEqual({ count: 0 })
- expect(renderCount.current).toEqual(1)
- })
-
- it('should handle setting state with partial updates', () => {
- const appState = create({ count: 0, name: 'John' })
- const renderCount = { current: 0 }
-
- const result = renderHook(() => {
- renderCount.current++
- return appState()
- })
-
- act(() => {
- appState.updateState({ count: 1 })
- })
-
- expect(result.result.current).toEqual({ count: 1, name: 'John' })
- expect(renderCount.current).toEqual(2)
- })
-
- it('should handle resetting state after multiple updates', () => {
- const appState = create({ count: 0 })
- const renderCount = { current: 0 }
-
- const result = renderHook(() => {
- renderCount.current++
- return appState()
- })
-
- act(() => {
- appState.setState({ count: 1 })
- appState.setState({ count: 2 })
- appState.reset()
- })
-
- expect(result.result.current).toEqual({ count: 0 })
- expect(renderCount.current).toEqual(2)
- })
-
- it('should handle concurrent asynchronous state updates', async () => {
- const appState = create({ count: 0 })
- const renderCount = { current: 0 }
-
- const result = renderHook(() => {
- renderCount.current++
- return appState()
- })
-
- act(() => {
- // Simulate concurrent asynchronous updates
- appState.setState({ count: 1 })
- appState.setState({ count: 2 })
- appState.setState({ count: 3 })
- })
-
- await waitFor(() => {
- expect(result.result.current).toEqual({ count: 3 })
- })
-
- expect(renderCount.current).toBe(2)
- })
-})
diff --git a/src/__tests__/types.test.ts b/src/__tests__/types.test.ts
deleted file mode 100644
index ee5ddc7..0000000
--- a/src/__tests__/types.test.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { getDefaultValue } from '../types'
-
-describe('getDefaultValue', () => {
- it('should return the value if it is not a function or promise', () => {
- expect(getDefaultValue(123)).toBe(123)
- })
-
- it('should return the resolved value if it is a function', () => {
- expect(getDefaultValue(() => 123)).toBe(123)
- })
-
- it('should return the promise if it is a promise', () => {
- const promise = Promise.resolve(123)
- expect(getDefaultValue(promise)).toBe(promise)
- expect(getDefaultValue(() => promise)).toBe(promise)
- })
-})
diff --git a/src/common.ts b/src/common.ts
deleted file mode 100644
index 2c3f612..0000000
--- a/src/common.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { useSyncExternalStoreWithSelector as useSync } from 'use-sync-external-store/shim/with-selector'
-import type { Emitter } from './create-emitter'
-import type { IsEqual } from './types'
-import { useDebugValue } from 'react'
-
-/**
- * Todo need to remove this
- */
-export function toType(object?: unknown): T {
- return object as T
-}
-
-export function useSyncExternalStore(
- emitter: Emitter,
- selector: (stateValue: T) => S,
- isEqual?: IsEqual,
-): undefined extends S ? T : S {
- const value = useSync(
- emitter.subscribe,
- emitter.getSnapshot,
- emitter.getSnapshot,
- selector ? (stateValue) => selector(stateValue) : toType,
- isEqual,
- ) as undefined extends S ? T : S
-
- useDebugValue(value)
- return value
-}
-// eslint-disable-next-line no-shadow
-export enum Abort {
- Error = 'StateAbortError',
-}
-/**
- * Cancelable promise function, return promise and controller
- */
-export function cancelablePromise(
- promise: Promise,
- previousController?: AbortController,
-): {
- promise: Promise
- controller: AbortController
-} {
- if (previousController) {
- previousController.abort()
- }
- const controller = new AbortController()
- const { signal } = controller
-
- const cancelable = new Promise((resolve, reject) => {
- // Listen for the abort event
- signal.addEventListener('abort', () => {
- reject(new DOMException('Promise was aborted', Abort.Error))
- })
-
- // When the original promise settles, resolve or reject accordingly
- promise.then(resolve).catch(reject)
- })
-
- return { promise: cancelable, controller }
-}
diff --git a/src/create-base-state.ts b/src/create-base-state.ts
deleted file mode 100644
index 46ebf4b..0000000
--- a/src/create-base-state.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import type { Emitter } from './create-emitter'
-import { select } from './select'
-import type { BaseState, GetterState } from './types'
-
-interface Options {
- readonly emitter: Emitter
- readonly reset: () => void
- readonly getState: () => T
- readonly getGetterState: () => GetterState
-}
-export function createBaseState(options: Options): BaseState {
- const { emitter, getGetterState, reset, getState } = options
- return {
- getState,
- reset,
- select(selector, isSame) {
- const state = getGetterState()
- return select(state, selector, isSame)
- },
-
- __internal: {
- emitter,
- },
- subscribe(listener) {
- listener(getState())
- return emitter.subscribe(() => {
- listener(getState())
- })
- },
- }
-}
diff --git a/src/create-emitter.ts b/src/create-emitter.ts
deleted file mode 100644
index fb3a264..0000000
--- a/src/create-emitter.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-export type EmitterSubscribe = (listener: (...params: P[]) => void) => () => void
-export interface Emitter {
- subscribe: EmitterSubscribe
- getSnapshot: () => R
- emit: (...params: P[]) => void
-}
-
-export function createEmitter(getSnapshot: () => R): Emitter {
- const listeners = new Set<(...params: P[]) => void>()
- return {
- subscribe: (listener) => {
- listeners.add(listener)
- return () => {
- listeners.delete(listener)
- }
- },
- emit: (...params) => {
- for (const listener of listeners) {
- listener(...params)
- }
- },
- getSnapshot,
- }
-}
diff --git a/src/create-getter-state.ts b/src/create-getter-state.ts
deleted file mode 100644
index 36123fc..0000000
--- a/src/create-getter-state.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import type { BaseState, GetterState } from './types'
-import { useStateValue } from './use-state-value'
-
-interface Options {
- readonly baseState: BaseState
-}
-export function createGetterState(options: Options): GetterState {
- const { baseState } = options
- const useSliceState: GetterState = (useSelector, isEqualHook) => {
- return useStateValue(useSliceState, useSelector, isEqualHook)
- }
- useSliceState.__internal = baseState.__internal
- useSliceState.getState = baseState.getState
- useSliceState.reset = baseState.reset
- useSliceState.select = baseState.select
- useSliceState.subscribe = baseState.subscribe
- return useSliceState
-}
diff --git a/src/create.ts b/src/create.ts
deleted file mode 100644
index 2071c59..0000000
--- a/src/create.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import { createEmitter } from './create-emitter'
-import type { SetValue, SetterState, StateDataInternal, DefaultValue, GetterState, IsEqual, UpdateValue } from './types'
-import { getDefaultValue } from './types'
-import { isAbortError, isEqualBase, isObject, isPromise, isSetValueFunction } from './is'
-import { createBaseState } from './create-base-state'
-import { createGetterState } from './create-getter-state'
-import { cancelablePromise } from './common'
-
-/**
- * Creates a basic atom state.
- * @param defaultValue - The initial state value.
- * @param options - Optional settings for the state (e.g., isEqual, onSet).
- * @returns A state object that can be used as a hook and provides state management methods.
- * @example
- * ```typescript
- * // Global scope
- * const counterState = state(0);
- * const userState = state({ name: 'John', age: 20 });
- *
- * // React component
- * const counter = counterState(); // Use as a hook
- * const user = userState();
- *
- * // Access partial data from the state using slice
- * const userAge = userState.slice((state) => state.age)();
- * ```
- */
-
-export function create(defaultValue: DefaultValue, isEqual: IsEqual = isEqualBase): SetterState> {
- function resolveSetter(value: T, stateSetter: SetValue): T {
- if (isSetValueFunction(stateSetter)) {
- return stateSetter(value)
- }
- return stateSetter
- }
-
- const stateData: StateDataInternal = {
- updateVersion: 0,
- value: undefined,
- }
-
- function getValue(): T {
- if (stateData.value === undefined) {
- stateData.value = getDefaultValue(defaultValue)
- }
- return stateData.value
- }
-
- function get(): T {
- const stateValue = getValue()
- if (isPromise(stateValue)) {
- const { controller, promise } = cancelablePromise(stateValue, stateData.abortController)
- stateData.abortController = controller
- promise
- .then((data) => {
- stateData.value = data as Awaited
- emitter.emit()
- })
- .catch((error) => {
- if (isAbortError(error)) {
- return
- }
- stateData.value = new Error(error) as T
- })
- }
- return stateValue
- }
-
- function set(stateValue: SetValue) {
- const stateValueData = getValue()
- if (stateData.abortController) {
- stateData.abortController.abort()
- stateData.abortController = undefined
- }
-
- const newState = resolveSetter(stateValueData, stateValue)
- const isEqualResult = isEqual?.(stateValueData, newState)
- if (isEqualResult || newState === stateValueData) {
- return
- }
- stateData.updateVersion++
- stateData.value = newState
- emitter.emit()
- }
-
- function update(stateValue: UpdateValue) {
- if (isObject(stateValue)) {
- return set((previousState) => {
- return { ...previousState, ...stateValue }
- })
- }
- set(stateValue as T)
- }
-
- const emitter = createEmitter(get)
-
- const baseState = createBaseState({
- emitter,
- getGetterState: () => setterState,
- getState: get,
- reset() {
- const value = getDefaultValue(defaultValue)
- if (isPromise(value)) {
- const { controller, promise } = cancelablePromise(value, stateData.abortController)
- stateData.abortController = controller
- promise
- .then((data) => {
- set(data as T)
- })
- .catch((error) => {
- if (isAbortError(error)) {
- return
- }
- stateData.value = new Error(error) as T
- })
- return
- }
- set(value)
- },
- })
-
- const getterState: GetterState = createGetterState({ baseState })
- const setterState: SetterState = getterState as SetterState
- setterState.setState = set
- setterState.updateState = update
- return setterState as SetterState>
-}
diff --git a/src/index.ts b/src/index.ts
deleted file mode 100644
index 546ab72..0000000
--- a/src/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export * from './types'
-export { create } from './create'
-export { select } from './select'
-export { merge } from './merge'
-export { useStateValue } from './use-state-value'
-export { shallow } from './shallow'
diff --git a/src/merge.ts b/src/merge.ts
deleted file mode 100644
index 624ee5f..0000000
--- a/src/merge.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { createBaseState } from './create-base-state'
-import { createEmitter } from './create-emitter'
-import { createGetterState } from './create-getter-state'
-import { isEqualBase } from './is'
-import type { IsEqual, GetterState } from './types'
-
-export function merge(
- states: { [K in keyof T]: GetterState },
- selector: (...values: T) => S,
- isEqual: IsEqual = isEqualBase,
-): GetterState {
- let previousData: S | undefined
- const emitter = createEmitter(() => {
- const data = selector(...(states.map((state) => state.getState()) as T))
- if (previousData !== undefined && isEqual(previousData, data)) {
- return previousData
- }
- previousData = data
- return data
- })
- for (const state of states) {
- state.__internal.emitter.subscribe(() => {
- emitter.emit()
- })
- }
-
- const baseState = createBaseState({
- emitter,
- getGetterState: () => getterState,
- getState: () => selector(...(states.map((state) => state.getState()) as T)),
- reset() {
- for (const state of states) state.reset()
- },
- })
-
- const getterState: GetterState = createGetterState({ baseState })
- return getterState
-}
diff --git a/src/select.ts b/src/select.ts
deleted file mode 100644
index 721ea3b..0000000
--- a/src/select.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { createBaseState } from './create-base-state'
-import { createEmitter } from './create-emitter'
-import { createGetterState } from './create-getter-state'
-import { isEqualBase } from './is'
-import type { IsEqual, GetterState } from './types'
-
-export function select(
- state: GetterState,
- selector: (value: T) => S,
- isEqual: IsEqual = isEqualBase,
-): GetterState {
- let previousData: S | undefined
- const emitter = createEmitter(() => {
- const data = selector(state.getState())
- if (previousData !== undefined && isEqual(previousData, data)) {
- return previousData
- }
- previousData = data
- return data
- })
- state.__internal.emitter.subscribe(() => {
- emitter.emit()
- })
-
- const baseState = createBaseState({
- emitter,
- getGetterState: () => getterState,
- getState: () => selector(state.getState()),
- reset: state.reset,
- })
- const getterState: GetterState = createGetterState({ baseState })
- return getterState
-}
diff --git a/src/types.ts b/src/types.ts
deleted file mode 100644
index c38fdea..0000000
--- a/src/types.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import type { Emitter } from './create-emitter'
-import { isFunction, isPromise } from './is'
-
-/**
- * Equality check function.
- */
-export type IsEqual = (a: T, b: T) => boolean
-
-export type Setter = (value: T) => T
-/**
- * Set new state value function.
- */
-export type SetValue = T | Setter
-export type UpdateValue = T extends object ? Partial : T
-
-/**
- * Set new state function
- */
-export type StateValue = undefined extends S ? T : S
-export type Set = (value: SetValue) => void
-export type Update = (value: UpdateValue) => void
-
-/**
- * Getting state value function.
- */
-export type GetState = () => T
-export interface StateDataInternal {
- value?: T
- updateVersion: number
- abortController?: AbortController
-}
-
-// eslint-disable-next-line no-shadow
-export enum StateKeys {
- IS_STATE = 'isState',
- IS_SLICE = 'isSlice',
-}
-
-export interface BaseState {
- /**
- * Reset state to default value if it's basic atom - if it's family - it will clear all family members
- */
- reset: () => void
- /**
- * Get current state value
- */
- getState: GetState
-
- select: (selector: (value: T) => S, isEqual?: IsEqual) => GetterState
-
- /**
- * Internal state data
- */
- __internal: {
- emitter: Emitter
- }
-
- subscribe: (listener: (value: T) => void) => () => void
-}
-
-export interface GetterState extends BaseState {
- // use use as the function call here
- (selector?: (state: T) => S, isEqual?: IsEqual): StateValue
-}
-export interface SetterState extends GetterState {
- /**
- * Set new state value
- */
- setState: Set
-
- /**
- * Set new state value
- */
- updateState: Update
-}
-
-export type State = SetterState | GetterState
-
-export type DefaultValue = T | (() => T)
-
-export function getDefaultValue(initValue: DefaultValue): T {
- if (isPromise(initValue)) {
- return initValue
- }
- if (isFunction(initValue)) {
- return (initValue as () => T)()
- }
- return initValue
-}
-
-export interface Ref {
- current: T | undefined
- readonly isRef: true
-}
diff --git a/src/use-state-value.ts b/src/use-state-value.ts
deleted file mode 100644
index 64b99a9..0000000
--- a/src/use-state-value.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import type { IsEqual, State } from './types'
-import { useSyncExternalStore, toType } from './common'
-import { isAnyOtherError, isPromise } from './is'
-
-/**
- * useCachedStateValue Hook.
- * Hook for use state inside react scope. If the state is async - component need to be wrapped with Suspense.
- * @param state - state value
- * @param selector - selector function (useStateValue(state, (state) => state.value)) - it return only selected value, selector don't need to be memoized.
- * @param isEqual - equality check function for selector
- * @returns StateValue from selector if provided, otherwise whole state
- */
-export function useStateValue(
- state: State,
- selector: (stateValue: T) => S = (stateValue) => toType(stateValue),
- isEqual?: IsEqual,
-): undefined extends S ? T : S {
- const data = useSyncExternalStore(
- state.__internal.emitter,
- (stateValue) => {
- return selector(stateValue)
- },
- isEqual,
- )
- if (isPromise(data)) {
- throw data
- }
- if (isAnyOtherError(data)) {
- throw data
- }
- return data
-}
diff --git a/tsconfig.json b/tsconfig.json
index e1ecd2b..8bb68f2 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -21,5 +21,5 @@
"downlevelIteration": true,
"sourceMap": false
},
- "include": ["src"]
+ "include": ["packages/core"]
}
diff --git a/tsconfig.types.json b/tsconfig.types.json
index 794485e..a26b355 100644
--- a/tsconfig.types.json
+++ b/tsconfig.types.json
@@ -7,7 +7,7 @@
"emitDeclarationOnly": false, // Generate only declaration files
"skipLibCheck": true
},
- "include": ["src"],
+ "include": ["packages/core"],
// exclude tests
"exclude": ["**/*.test.ts", "**/*.test.tsx"]
}