├── test
├── useForm.js
├── index.html
├── enhook.js
├── useCountdown.js
├── register.js
├── usePrevious.js
├── useThrottle.js
├── useObservable.js
├── index.js
├── useSyncEffect.js
├── useEffect.js
├── useCookie.js
├── useInput.js
├── useState.js
├── useAction.js
├── useValidate.js
├── useAttribute.js
├── useGlobalCache.js
├── useProperty.js
├── useStorage.js
├── useChannel.js
├── useStore.js
├── useSearchParam.js
└── useFormField.js
├── .travis.yml
├── .gitattributes
├── LICENSE
├── .gitignore
├── package.json
├── index.js
└── readme.md
/test/useForm.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "12"
4 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/test/enhook.js:
--------------------------------------------------------------------------------
1 | import enhook from 'enhook'
2 | import setHooks from '../'
3 |
4 | import * as preact from 'preact'
5 | import * as hooks from 'preact/hooks'
6 | enhook.use(preact)
7 | setHooks(hooks)
8 |
9 | // import * as atomico from 'atomico'
10 | // enhook.use(atomico)
11 | // setHooks(atomico)
12 |
13 | // import * as aug from 'augmentor'
14 | // enhook.use(aug)
15 | // setHooks(aug)
16 |
17 |
18 | export default enhook
19 |
--------------------------------------------------------------------------------
/test/useCountdown.js:
--------------------------------------------------------------------------------
1 | import t from 'tst'
2 | import enhook from './enhook.js'
3 | import { useCountdown } from '../'
4 | import { time } from 'wait-please'
5 |
6 | t('useCountdown: basics', async t => {
7 | let log = []
8 | let f = enhook(() => {
9 | let [n, reset] = useCountdown(3, 50)
10 | log.push(n)
11 | })
12 | f()
13 |
14 | t.deepEqual(log, [ 3 ])
15 | await time(300)
16 | t.deepEqual(log, [ 3, 2, 1, 0 ])
17 |
18 | t.end()
19 | })
20 |
--------------------------------------------------------------------------------
/test/register.js:
--------------------------------------------------------------------------------
1 | let { JSDOM } = require('jsdom')
2 |
3 | const { window } = new JSDOM(``, {
4 | url: "http://localhost/",
5 | storageQuota: 10000000,
6 | pretendToBeVisual: true,
7 | FetchExternalResources: false,
8 | ProcessExternalResources: false
9 | })
10 |
11 | let props = Object.getOwnPropertyNames(window)
12 |
13 | props.forEach(prop => {
14 | if (prop in global) return
15 | Object.defineProperty(global, prop, {
16 | configurable: true,
17 | get: () => window[prop]
18 | })
19 | })
20 |
--------------------------------------------------------------------------------
/test/usePrevious.js:
--------------------------------------------------------------------------------
1 | import t from 'tst'
2 | import enhook from './enhook.js'
3 | import { usePrevious, useState } from '../'
4 | import { tick } from 'wait-please'
5 |
6 | t('usePrevious: basics', async t => {
7 | let log = []
8 | let f = enhook(() => {
9 | let [foo, setFoo] = useState(1)
10 | let fooPrev = usePrevious(foo)
11 | log.push(foo, fooPrev)
12 | })
13 | f()
14 |
15 | t.deepEqual(log, [ 1, undefined ])
16 |
17 | await tick()
18 | f()
19 | t.deepEqual(log, [ 1, undefined, 1, 1 ])
20 |
21 | t.end()
22 | })
23 |
--------------------------------------------------------------------------------
/test/useThrottle.js:
--------------------------------------------------------------------------------
1 | import t from 'tst'
2 | import enhook from './enhook.js'
3 | import { useThrottle } from '../src/index'
4 | import { time } from 'wait-please'
5 |
6 | t.skip('useThrottle: basics', async t => {
7 | let log = []
8 | let f = enhook((x) => {
9 | let [v] = useThrottle(x, 50)
10 | log.push(v)
11 | })
12 | f(1)
13 | f(2)
14 | t.deepEqual(log, [ 1 ])
15 | await time(10)
16 | t.deepEqual(log, [ 1 ])
17 | await time(40)
18 | t.deepEqual(log, [ 1, 2 ])
19 | await time(50)
20 | t.deepEqual(log, [ 1, 2 ])
21 |
22 | t.end()
23 | })
24 |
--------------------------------------------------------------------------------
/test/useObservable.js:
--------------------------------------------------------------------------------
1 | import t from 'tst'
2 | import enhook from './enhook.js'
3 | import { tick, frame, time } from 'wait-please'
4 | import { value } from 'observable'
5 | import { useObservable } from '../index.js'
6 |
7 | t('useObservable: core', async t => {
8 | let log = []
9 | let v = value(0)
10 | let f = enhook(() => {
11 | let [val, setVal] = useObservable(v)
12 | log.push(val)
13 | setTimeout(() => setVal(1))
14 | })
15 |
16 | f()
17 | await frame()
18 | t.is(log, [0])
19 |
20 | await time(15)
21 | t.is(log, [0, 1])
22 | t.is(v(), 1)
23 |
24 | t.end()
25 | })
26 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | import('./useChannel').then()
2 | import('./useAction').then()
3 | import('./useStorage').then()
4 | import('./useSearchParam').then()
5 | import('./useCountdown').then()
6 | import('./usePrevious').then()
7 | import('./useFormField').then()
8 | import('./useInput').then()
9 | import('./useValidate').then()
10 | import('./useObservable').then()
11 | // import('./useStore').then()
12 | // import('./useGlobalCache').then()
13 | // import('./useLocalStorage').then()
14 | // import('./useProperty').then()
15 | // import('./useState').then()
16 | // import('./useEffect').then()
17 | // import('./useSyncEffect').then()
18 | // import('./useCookie').then()
19 | // import('./useThrottle').then()
20 | // import('./useAttribute').then()
21 |
22 |
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 dy
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 OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/test/useSyncEffect.js:
--------------------------------------------------------------------------------
1 | import t from 'tst'
2 | import enhook from './enhook.js'
3 | import { useSyncEffect } from '../src/index'
4 | import { tick, frame } from 'wait-please'
5 |
6 | t('useSyncEffect: useInit', t => {
7 | let log = []
8 |
9 | let f = enhook((i) => {
10 | useSyncEffect(() => {
11 | log.push('on')
12 | return () => log.push('off')
13 | }, [])
14 | })
15 |
16 | f(1)
17 | t.deepEqual(log, ['on'])
18 | f(2)
19 | t.deepEqual(log, ['on'])
20 |
21 | t.end()
22 | })
23 |
24 | t('useSyncEffect: destructor is fine ', t => {
25 | let log = []
26 |
27 | let f = enhook((i) => {
28 | useSyncEffect(() => {
29 | log.push('on')
30 | return () => log.push('off')
31 | }, [i])
32 | })
33 |
34 | f(1)
35 | t.deepEqual(log, ['on'])
36 | f(2)
37 | t.deepEqual(log, ['on', 'off', 'on'])
38 |
39 | t.end()
40 | })
41 |
42 | t('useSyncEffect: async fn is fine', async t => {
43 | let log = []
44 |
45 | let f = enhook((i) => {
46 | useSyncEffect(async () => {
47 | await tick(2)
48 | log.push(1)
49 | }, [])
50 | })
51 |
52 | f()
53 | await tick(3)
54 | t.deepEqual(log, [1])
55 | f()
56 | await tick(3)
57 | t.deepEqual(log, [1])
58 |
59 | t.end()
60 | })
61 |
--------------------------------------------------------------------------------
/test/useEffect.js:
--------------------------------------------------------------------------------
1 | import t from 'tst'
2 | import enhook from './enhook.js'
3 | import { useEffect } from '../src/index'
4 | import { tick, frame } from 'wait-please'
5 |
6 | t('useEffect: microtask guaranteed', async t => {
7 | let log = []
8 |
9 | let f = (i) => {
10 | log.push('call', i)
11 | useEffect(() => {
12 | log.push('effect', i)
13 | return 1
14 | })
15 | }
16 |
17 | let f1 = enhook(f)
18 | let f2 = enhook(f)
19 | f1(1)
20 | f2(2)
21 | await tick(2)
22 | t.deepEqual(log, ['call', 1, 'call', 2, 'effect', 1, 'effect', 2])
23 |
24 | t.end()
25 | })
26 |
27 | t('useEffect: async fn is fine', async t => {
28 | let log = []
29 |
30 | let f = enhook((i) => {
31 | useEffect(async () => {
32 | await tick(2)
33 | log.push(1)
34 | }, [])
35 | })
36 |
37 | f()
38 | await tick(3)
39 | t.deepEqual(log, [1])
40 | f()
41 | await tick(3)
42 | t.deepEqual(log, [1])
43 |
44 | t.end()
45 | })
46 |
47 | t('useEffect: dispose should clean up effects', async t => {
48 | let log = []
49 | let f = enhook(() => {
50 | useEffect(() => {
51 | log.push('in')
52 | return () => log.push('out')
53 | })
54 | })
55 | f()
56 | await frame(2)
57 | f.unhook()
58 | await frame(2)
59 | t.deepEqual(log, ['in', 'out'])
60 |
61 | t.end()
62 | })
63 |
--------------------------------------------------------------------------------
/test/useCookie.js:
--------------------------------------------------------------------------------
1 | import t from 'tst'
2 | import { useCookie, useEffect } from '../src/index'
3 | import enhook from './enhook.js'
4 | import { tick, idle, frame } from 'wait-please'
5 | import * as cookie from 'es-cookie'
6 |
7 |
8 | t('useCookie: basic', async t => {
9 | cookie.set('foo', 'bar')
10 | let log = []
11 | let f = enhook(() => {
12 | let [foo, setFoo] = useCookie('foo')
13 | log.push(foo)
14 | useEffect(() => {
15 | setFoo('baz')
16 | }, [])
17 | })
18 | f()
19 | t.deepEqual(log, ['bar'])
20 | await frame(4)
21 | t.deepEqual(log, ['bar', 'baz'])
22 |
23 | cookie.remove('foo')
24 | t.end()
25 | })
26 |
27 | t('useCookie: multiple components use same key', async t => {
28 | cookie.set('count', 1)
29 | let log = []
30 |
31 | const f = (i, log) => {
32 | let [count, setCount] = useCookie('count', i)
33 | log.push('call', i, count)
34 | useEffect(() => {
35 | log.push('effect', i)
36 | setCount(i)
37 | }, [])
38 | }
39 | let f1 = enhook(f)
40 | let f2 = enhook(f)
41 |
42 | f1(1, log)
43 | t.deepEqual(log, ['call', 1, 1])
44 | await frame(2)
45 | t.deepEqual(log, ['call', 1, 1, 'effect', 1])
46 |
47 | f2(2, log)
48 | await frame(4)
49 | t.deepEqual(log, ['call', 1, 1, 'effect', 1, 'call', 2, 1, 'effect', 2, 'call', 2, 2, 'call', 1, 2])
50 |
51 | cookie.remove('count')
52 | t.end()
53 | })
54 |
--------------------------------------------------------------------------------
/test/useInput.js:
--------------------------------------------------------------------------------
1 | import t from 'tst'
2 | import { useInput, useEffect, useRef } from '../'
3 | import enhook from './enhook.js'
4 | import { tick, idle, frame, time } from 'wait-please'
5 | import { render } from 'preact'
6 | import { html } from 'htm/preact'
7 |
8 |
9 | t('useInput: element', async t => {
10 | let log = []
11 | let input = document.createElement('input')
12 | input.value = 'foo'
13 |
14 | let f = enhook(() => {
15 | let [v, setV] = useInput(input)
16 | log.push(v)
17 | useEffect(() => {
18 | setV('bar')
19 | }, [])
20 | })
21 | f()
22 |
23 | t.deepEqual(log, ['foo'])
24 |
25 | await frame(2)
26 |
27 | t.deepEqual(log, ['foo', 'bar'])
28 | t.equal(input.value, 'bar')
29 |
30 | input.value = 'baz'
31 | input.dispatchEvent(new Event('change'))
32 | await frame(2)
33 | t.deepEqual(log, ['foo', 'bar', 'baz'])
34 |
35 | f.unhook()
36 | t.end()
37 | })
38 |
39 |
40 | t('useInput: ref', async t => {
41 | let log = []
42 | let el = document.createElement('div')
43 |
44 | render(html`<${() => {
45 | let ref = useRef()
46 | let [v, setV] = useInput(ref)
47 | log.push(v)
48 | useEffect(() => {
49 | setV('foo')
50 |
51 | setTimeout(() => {
52 | setV(null)
53 | }, 50)
54 | }, [])
55 |
56 | return html``
57 | }}/>`, el)
58 |
59 | let input = el.querySelector('input')
60 | await frame(2)
61 | t.equal(input.value, 'foo')
62 | t.deepEqual(log, [undefined, 'foo'])
63 | await time(50)
64 | t.ok(!input.hasAttribute('value'))
65 |
66 | render(null, el)
67 |
68 | t.end()
69 | })
70 |
--------------------------------------------------------------------------------
/test/useState.js:
--------------------------------------------------------------------------------
1 | import t from 'tst'
2 | import enhook from './enhook.js'
3 | import { useState, useEffect, useCallback } from '../src/standard'
4 | import { tick, frame, time } from 'wait-please'
5 |
6 | t('useState: initial run once', async t => {
7 | let log = []
8 |
9 | let f = enhook(() => {
10 | let [v, setV] = useState((prev) => {
11 | log.push(prev)
12 | return 1
13 | })
14 | })
15 |
16 | f()
17 | t.deepEqual(log, [undefined])
18 |
19 | f()
20 | t.deepEqual(log, [undefined])
21 |
22 | t.end()
23 | })
24 |
25 | t('useState: unchanged value does not cause rerender', async t => {
26 | let log = []
27 | let x = 0
28 | let f = enhook(() => {
29 | let [v, setV] = useState(0)
30 | log.push(v)
31 | useEffect(() => {
32 | setV(() => x)
33 | }, [x])
34 | })
35 | f()
36 |
37 | t.deepEqual(log, [0])
38 |
39 | await frame(4)
40 |
41 | t.deepEqual(log, [0])
42 |
43 | t.end()
44 | })
45 |
46 | t('useState: deps reinit', async t => {
47 | let log = []
48 |
49 | let f = enhook((i) => {
50 | let [v, setV] = useState((prev) => {
51 | log.push(prev)
52 | return 1
53 | }, [i])
54 | })
55 |
56 | f(1)
57 | t.deepEqual(log, [undefined])
58 |
59 | f(2)
60 | t.deepEqual(log, [undefined, 1])
61 |
62 | t.end()
63 | })
64 |
65 | t.skip('useState: double set', async t => {
66 | let log = []
67 |
68 | let fn = enhook((i) => {
69 | let [count, setCount] = useState(i)
70 |
71 | log.push(count)
72 | useEffect(() => {
73 | setCount(i + 1)
74 | setCount(i)
75 | }, [])
76 | })
77 | fn(1)
78 | await frame(3)
79 | t.deepEqual(log, [1])
80 |
81 | t.end()
82 | })
83 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Optional REPL history
57 | .node_repl_history
58 |
59 | # Output of 'npm pack'
60 | *.tgz
61 |
62 | # Yarn Integrity file
63 | .yarn-integrity
64 |
65 | # dotenv environment variables file
66 | .env
67 | .env.test
68 |
69 | # parcel-bundler cache (https://parceljs.org/)
70 | .cache
71 |
72 | # next.js build output
73 | .next
74 |
75 | # nuxt.js build output
76 | .nuxt
77 |
78 | # vuepress build output
79 | .vuepress/dist
80 |
81 | # Serverless directories
82 | .serverless/
83 |
84 | # FuseBox cache
85 | .fusebox/
86 |
87 | # DynamoDB Local files
88 | .dynamodb/
89 |
90 | dist
91 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "unihooks",
3 | "version": "2.6.0",
4 | "description": "Universal poly-framework react hooks",
5 | "main": "index.js",
6 | "module": "index.js",
7 | "files": [
8 | "index.js"
9 | ],
10 | "dependencies": {
11 | "any-hooks": "^4.0.1"
12 | },
13 | "devDependencies": {
14 | "atomico": "^0.16.1",
15 | "augmentor": "^2.1.4",
16 | "dom-augmentor": "^2.0.3",
17 | "enhook": "^3.1.0",
18 | "es-dev-server": "^1.32.0",
19 | "esm": "^3.2.25",
20 | "haunted": "^4.6.3",
21 | "htm": "^3.0.1",
22 | "jsdom": "^16.0.1",
23 | "observable": "^2.1.4",
24 | "parcel": "^1.12.4",
25 | "preact": "^10.1.0",
26 | "tst": "^5.3.1",
27 | "wait-please": "^3.0.0"
28 | },
29 | "browser": {
30 | "./src/util/timers.js": "./src/util/browser-timers.js"
31 | },
32 | "browserslist": [
33 | "last 1 Chrome versions"
34 | ],
35 | "esm": {
36 | "force": true
37 | },
38 | "scripts": {
39 | "start:es": "es-dev-server --node-resolve --dedupe",
40 | "start": "parcel serve --no-hmr test/index.html",
41 | "test": "node -r esm -r ./test/register.js ./test/index.js"
42 | },
43 | "repository": {
44 | "type": "git",
45 | "url": "git+https://github.com/unihooks/unihooks.git"
46 | },
47 | "keywords": [
48 | "react",
49 | "hooks",
50 | "augmentor",
51 | "preact",
52 | "haunted",
53 | "atomico",
54 | "fuco",
55 | "rax",
56 | "useStore",
57 | "useStorage",
58 | "useQueryString",
59 | "useSearchParam",
60 | "useChannel"
61 | ],
62 | "author": "Dmitry Yv. ",
63 | "license": "MIT",
64 | "bugs": {
65 | "url": "https://github.com/unihooks/unihooks/issues"
66 | },
67 | "homepage": "https://github.com/unihooks/unihooks#readme"
68 | }
69 |
--------------------------------------------------------------------------------
/test/useAction.js:
--------------------------------------------------------------------------------
1 | import t from 'tst'
2 | import enhook from './enhook.js'
3 | import setHooks, { useAction, useEffect, useState, useChannel } from '../'
4 | import { tick, frame, time } from 'wait-please'
5 |
6 | t('useAction: basic', async t => {
7 | let log = []
8 |
9 | let f = enhook(() => {
10 | let [foo, setFoo] = useChannel('foo', { bar: 'baz' })
11 | log.push(foo)
12 | let [fooFn] = useAction('foo', () => {
13 | useEffect(() => {
14 | setFoo({ bar: 'qux' })
15 | }, [])
16 | })
17 | fooFn()
18 | })
19 | f()
20 | await tick(2)
21 | t.deepEqual(log, [{ bar: 'baz'}])
22 | f()
23 | t.deepEqual(log, [{ bar: 'baz'}, { bar: 'qux'}])
24 | await tick(2)
25 |
26 | f.unhook()
27 |
28 | t.end()
29 | })
30 |
31 | t('useAction: passes args & returns result', async t => {
32 | let log = []
33 | let f = enhook(() => {
34 | let action = useAction('args', function (...args) {
35 | log.push(...args)
36 | return 4
37 | })
38 | log.push(action(1, 2, 3))
39 | })
40 | f()
41 |
42 | t.deepEqual(log, [1, 2, 3, 4])
43 |
44 | f.unhook()
45 | t.end()
46 | })
47 |
48 | t('useAction: null value', async t => {
49 | let log = []
50 | let f1 = enhook(() => {
51 | let a = useAction('x')
52 | log.push(a[0])
53 | })
54 | let f2 = enhook(() => {
55 | let a = useAction('x', 123)
56 | log.push(a[0])
57 | })
58 | f1()
59 | f2()
60 |
61 | t.deepEqual(log,[undefined, 123])
62 |
63 | f1.unhook()
64 | f2.unhook()
65 |
66 | t.end()
67 | })
68 |
69 | t('useAction: should support deps', async t => {
70 | let log = []
71 | let f = enhook((arg) => {
72 | let act = useAction('x', () => log.push(arg), [arg])
73 | act()
74 | })
75 | f(1)
76 | f(2)
77 |
78 | t.deepEqual(log, [1, 2])
79 | f.unhook()
80 |
81 | t.end()
82 | })
83 |
--------------------------------------------------------------------------------
/test/useValidate.js:
--------------------------------------------------------------------------------
1 | import t from 'tst'
2 | import enhook from './enhook.js'
3 | import { useValidate, useEffect } from '../'
4 | import { tick, frame, time } from 'wait-please'
5 |
6 | t('useValidate: validates single', async t => {
7 | let log = []
8 |
9 | let f = enhook((arg) => {
10 | let [err, validate] = useValidate(arg => !!arg)
11 | log.push(arg, validate(arg), err)
12 | })
13 |
14 | f('')
15 | await time(10)
16 | t.deepEqual(log, ['', false, null, '', false, false])
17 |
18 | log = []
19 | f('ok')
20 | await time(10)
21 | t.deepEqual(log, ['ok', true, false, 'ok', true, null])
22 |
23 | t.end()
24 | })
25 |
26 | t('useValidate: validates array', async t => {
27 | let log = []
28 |
29 | let f = enhook((arg) => {
30 | let [err, validate] = useValidate([v => !!v, v => v === 'b'])
31 | log.push(arg, validate(arg), err)
32 | })
33 |
34 | f('')
35 | await time(10)
36 | t.deepEqual(log, ['', false, null, '', false, false])
37 |
38 | log = []
39 | f('a')
40 | await time(10)
41 | t.deepEqual(log, ['a', false, false])
42 |
43 | log = []
44 | f('b')
45 | await frame(2)
46 | t.deepEqual(log, ['b', true, false, 'b', true, null])
47 |
48 | t.end()
49 | })
50 |
51 | t.skip('useValidate: error in validator', async t => {
52 | let log = []
53 |
54 | let f = enhook((arg) => {
55 | let [err, validate] = useValidate(arg => xxx)
56 | log.push(arg, validate(arg), err)
57 | })
58 |
59 | f('')
60 | await time(10)
61 | log[5] = log[5].name
62 | t.deepEqual(log, ['', false, null, '', false, 'ReferenceError'])
63 |
64 | t.end()
65 | })
66 |
67 | t('useValidate: error in array', async t => {
68 | let log = []
69 |
70 | let f = enhook((arg) => {
71 | let [err, validate] = useValidate([v => true, v => v === 'b' ? true : 'ugh'])
72 | log.push(arg, validate(arg), err)
73 | })
74 |
75 | await time(10)
76 |
77 | log = []
78 | f('a')
79 | await time(10)
80 | t.deepEqual(log, ["a", false, null, 'a', false, 'ugh'])
81 |
82 | t.end()
83 | })
84 |
--------------------------------------------------------------------------------
/test/useAttribute.js:
--------------------------------------------------------------------------------
1 | import t from 'tst'
2 | import enhook from './enhook.js'
3 | import { tick, frame } from 'wait-please'
4 | import { useAttribute, useEffect, useRef } from '../src/index'
5 |
6 | t('useAttribute: basics', async t => {
7 | let log = []
8 | let el = document.createElement('div')
9 | el.setAttribute('foo', 'bar')
10 | let f = enhook(() => {
11 | let [foo, setFoo] = useAttribute(el, 'foo')
12 | log.push(foo)
13 | useEffect(() => {
14 | setFoo('baz')
15 | }, [])
16 | })
17 |
18 | f()
19 |
20 | t.deepEqual(log, ['bar'])
21 | await tick(2)
22 | t.deepEqual(log, ['bar', 'baz'])
23 | t.equal(el.getAttribute('foo'), 'baz')
24 |
25 | el.setAttribute('foo', 'qux')
26 | await tick(2)
27 | t.deepEqual(log, ['bar', 'baz', 'qux'])
28 |
29 | el.setAttribute('bar', 'baz')
30 | await tick(2)
31 | t.deepEqual(log, ['bar', 'baz', 'qux'])
32 |
33 | t.end()
34 | })
35 |
36 | t('useAttribute: handle ref', async t => {
37 | let log = []
38 |
39 | let el = document.createElement('div')
40 | el.setAttribute('foo', 'bar')
41 |
42 | let f = enhook(() => {
43 | let ref = useRef()
44 | let [foo, setFoo] = useAttribute(ref, 'foo')
45 | log.push(foo)
46 | ref.current = el
47 |
48 | useEffect(() => () => setFoo())
49 | })
50 | f()
51 |
52 | await frame(1)
53 | t.deepEqual(log, [null])
54 | f()
55 | await frame(2)
56 | t.deepEqual(log, [null, 'bar'])
57 |
58 | f.unhook()
59 | await frame(2)
60 | t.notOk(el.hasAttribute('foo'))
61 |
62 | t.end()
63 | })
64 |
65 | t('useAttribute: properly read initial values', async t => {
66 | let log = []
67 |
68 | let el = document.createElement('div')
69 | el.setAttribute('a', '')
70 | el.setAttribute('c', 'xyz')
71 | document.body.appendChild(el)
72 |
73 | enhook(() => {
74 | let [a] = useAttribute(el, 'a')
75 | let [b] = useAttribute(el, 'b')
76 | let [c] = useAttribute(el, 'c')
77 | t.equal(a, true)
78 | t.equal(b, null)
79 | t.equal(c, 'xyz')
80 | })()
81 |
82 | t.end()
83 | })
84 |
--------------------------------------------------------------------------------
/test/useGlobalCache.js:
--------------------------------------------------------------------------------
1 | import t from 'tst'
2 | import { useGlobalCache, useEffect } from '../src/index'
3 | import enhook from './enhook.js'
4 | import { tick, idle, frame } from 'wait-please'
5 | import globalCache from 'global-cache'
6 |
7 |
8 | t('useGlobalCache: basic', async t => {
9 | globalCache.set('count', 0)
10 | let log = []
11 | let f = enhook(() => {
12 | let [count, setCount] = useGlobalCache('count')
13 | log.push(count)
14 | useEffect(() => {
15 | setCount(1)
16 | }, [])
17 | })
18 | f()
19 | t.deepEqual(log, [0])
20 | await frame(4)
21 | t.deepEqual(log, [0, 1])
22 |
23 | globalCache.delete('count')
24 | t.end()
25 | })
26 |
27 | t('useGlobalCache: multiple components use same key', async t => {
28 | let log = []
29 |
30 | const f = (i, log) => {
31 | let [count, setCount] = useGlobalCache('count', i)
32 | log.push('call', i, count)
33 | useEffect(() => {
34 | log.push('effect', i)
35 | setCount(i)
36 | }, [])
37 | }
38 | let f1 = enhook(f)
39 | let f2 = enhook(f)
40 |
41 | f1(1, log)
42 | t.deepEqual(log, ['call', 1, 1])
43 | await frame(2)
44 | t.deepEqual(log, ['call', 1, 1, 'effect', 1])
45 |
46 | f2(2, log)
47 | await frame(4)
48 | t.deepEqual(log, ['call', 1, 1, 'effect', 1, 'call', 2, 1, 'effect', 2, 'call', 2, 2, 'call', 1, 2])
49 |
50 | globalCache.delete('count')
51 | t.end()
52 | })
53 |
54 | t('useGlobalCache: does not trigger unchanged updates', async t => {
55 | let log = []
56 | let f = enhook((i) => {
57 | let [count, setCount] = useGlobalCache('count', i)
58 | log.push(count)
59 | useEffect(() => {
60 | setCount(i + 1)
61 | setCount(i)
62 | }, [])
63 | })
64 | f(1)
65 | t.deepEqual(log, [1])
66 | await frame(3)
67 | t.deepEqual(log, [1])
68 |
69 | globalCache.delete('count')
70 | t.end()
71 | })
72 |
73 | t('useGlobalCache: must be writable', async t => {
74 | globalCache.set('x', 1)
75 |
76 | let log = []
77 |
78 | enhook(() => {
79 | let [x, setX] = useGlobalCache('x')
80 | log.push(x)
81 | setX(2)
82 | })()
83 |
84 | t.deepEqual(log, [1])
85 | await tick(3)
86 | t.deepEqual(log, [1, 2])
87 |
88 | globalCache.delete('x')
89 | t.end()
90 | })
91 |
--------------------------------------------------------------------------------
/test/useProperty.js:
--------------------------------------------------------------------------------
1 | import t from 'tst'
2 | import { useProperty, useEffect } from '../src/index'
3 | import enhook from './enhook.js'
4 | import { tick, idle, frame } from 'wait-please'
5 |
6 |
7 | t('useProperty: basic', async t => {
8 | let obj = {count: 0}
9 |
10 | let log = []
11 | let f = enhook(() => {
12 | let [count, setCount] = useProperty(obj, 'count')
13 | log.push(count)
14 | useEffect(() => {
15 | setCount(1)
16 | }, [])
17 | })
18 | f()
19 | t.deepEqual(log, [0])
20 | await frame(4)
21 | t.deepEqual(log, [0, 1])
22 | t.deepEqual(obj, {count: 1})
23 |
24 | t.end()
25 | })
26 |
27 | t('useProperty: multiple components use same key', async t => {
28 | let log = []
29 |
30 | let obj = {count: null}
31 |
32 | const f = (i, log) => {
33 | let [count, setCount] = useProperty(obj, 'count', i)
34 | log.push('call', i, count)
35 | useEffect(() => {
36 | log.push('effect', i)
37 | setCount(i)
38 | }, [])
39 | }
40 | let f1 = enhook(f)
41 | let f2 = enhook(f)
42 |
43 | f1(1, log)
44 | t.deepEqual(log, ['call', 1, 1])
45 | await frame(2)
46 | t.deepEqual(log, ['call', 1, 1, 'effect', 1])
47 |
48 | f2(2, log)
49 | await frame(4)
50 | t.deepEqual(log, ['call', 1, 1, 'effect', 1, 'call', 2, 1, 'effect', 2, 'call', 2, 2, 'call', 1, 2])
51 |
52 | t.end()
53 | })
54 |
55 | t('useProperty: does not trigger unchanged updates', async t => {
56 | let obj = {count: null}
57 | let log = []
58 | let f = enhook((i) => {
59 | let [count, setCount] = useProperty(obj, 'count', i)
60 | log.push(count)
61 | useEffect(() => {
62 | setCount(i + 1)
63 | setCount(i)
64 | }, [])
65 | })
66 | f(1)
67 | t.deepEqual(log, [1])
68 | await frame(2)
69 | t.deepEqual(log, [1])
70 |
71 | t.end()
72 | })
73 |
74 | t('useProperty: must be writable', async t => {
75 | let log = []
76 | let obj = { x : 1 }
77 |
78 | enhook(() => {
79 | let [x, setX] = useProperty(obj, 'x')
80 | log.push(x)
81 | setX(2)
82 | })()
83 |
84 | t.deepEqual(log, [1])
85 | await tick(3)
86 | t.deepEqual(log, [1, 2])
87 |
88 | t.end()
89 | })
90 |
91 | t('useProperty: keeps prev setter/getter', async t => {
92 | let log = []
93 | let obj = {
94 | _x: 0,
95 | get x() {
96 | log.push('get', this._x); return this._x
97 | },
98 | set x(x) {
99 | log.push('set', x);
100 | this._x = x
101 | }
102 | }
103 |
104 | let f = enhook(() => {
105 | let [prop, setProp] = useProperty(obj, 'x')
106 | log.push('call', prop)
107 | })
108 | f()
109 |
110 | t.deepEqual(log, ['get', 0, 'call', 0])
111 |
112 | obj.x
113 | await frame(2)
114 | t.deepEqual(log, ['get', 0, 'call', 0, 'get', 0])
115 |
116 | obj.x = 1
117 | await frame(3)
118 | t.deepEqual(log, ['get', 0, 'call', 0, 'get', 0, 'set', 1, 'call', 1])
119 |
120 | // log = []
121 | // xs.cancel()
122 | // t.deepEqual(log, [])
123 |
124 | // obj.x
125 | // t.deepEqual(log, ['get', 1])
126 |
127 | // obj.x = 0
128 | // await frame(2)
129 | // t.deepEqual(log, ['get', 1, 'set', 0])
130 |
131 | t.end()
132 | })
133 |
134 |
135 | t.skip('useProperty: observes path', async t => {
136 | t.end()
137 | })
138 |
139 |
140 |
141 |
--------------------------------------------------------------------------------
/test/useStorage.js:
--------------------------------------------------------------------------------
1 | import t from 'tst'
2 | import { useStorage, useEffect } from '../'
3 | import enhook from './enhook.js'
4 | import { tick, idle, frame } from 'wait-please'
5 |
6 | t('useStorage: basic', async t => {
7 | localStorage.removeItem('count')
8 |
9 | let log = []
10 | let f = enhook(() => {
11 | let [count, setCount] = useStorage('count', 0, {prefix: ''})
12 | log.push(count)
13 | useEffect(() => {
14 | setCount(1)
15 | }, [])
16 | })
17 | f()
18 | t.deepEqual(log, [0])
19 | await frame(4)
20 | t.deepEqual(log, [0, 1])
21 |
22 | await frame(4)
23 | f.unhook()
24 | await frame(4)
25 | localStorage.removeItem('count')
26 |
27 | t.end()
28 | })
29 |
30 | t('useStorage: should read stored value', async t => {
31 | localStorage.setItem('count', 1)
32 |
33 | let log = []
34 | let f = enhook(() => {
35 | let [count, setCount] = useStorage('count', null, { prefix: '' })
36 | log.push(count)
37 | })
38 | f()
39 | t.deepEqual(log, [1])
40 | await frame(4)
41 |
42 | f.unhook()
43 | await frame(4)
44 | localStorage.removeItem('count')
45 |
46 | t.end()
47 | })
48 |
49 | t('useStorage: multiple components use same key', async t => {
50 | let log = []
51 |
52 | localStorage.removeItem('count')
53 |
54 | const f = (i, log) => {
55 | let [count, setCount] = useStorage('count', i, {prefix: ''})
56 | log.push('call', i, count)
57 | useEffect(() => {
58 | log.push('effect', i)
59 | setCount(i)
60 | }, [])
61 | }
62 | let f1 = enhook(f)
63 | let f2 = enhook(f)
64 |
65 | f1(1, log)
66 | t.deepEqual(log, ['call', 1, 1])
67 | await frame(2)
68 | t.deepEqual(log, ['call', 1, 1, 'effect', 1, 'call', 1, 1])
69 |
70 | f2(2, log)
71 | await frame(4)
72 | t.deepEqual(log, ['call', 1, 1, 'effect', 1, 'call', 1, 1, 'call', 2, 1, 'effect', 2, 'call', 2, 2, 'call', 1, 2])
73 |
74 | f1.unhook()
75 | f2.unhook()
76 | await frame(4)
77 | localStorage.removeItem('count')
78 |
79 | t.end()
80 | })
81 |
82 | t('useStorage: does not trigger unchanged updates too many times', async t => {
83 | localStorage.removeItem('count')
84 |
85 | let log = []
86 | let f = enhook((i) => {
87 | let [count, setCount] = useStorage('count', i, { prefix: ''})
88 | log.push(count)
89 | useEffect(() => {
90 | setCount(i + 1)
91 | setCount(i + 2)
92 | setCount(i)
93 | }, [])
94 | })
95 | f(1)
96 | t.deepEqual(log, [1])
97 | await frame(2)
98 | t.deepEqual(log, [1, 1])
99 |
100 | f.unhook()
101 | await frame(4)
102 | localStorage.removeItem('count')
103 |
104 | t.end()
105 | })
106 |
107 | t('useStorage: fn init should be called with initial value', async t => {
108 | localStorage.setItem('count', 0)
109 |
110 | let log = []
111 | let f = enhook(() => {
112 | let [value] = useStorage('count', (count) => {
113 | log.push(count)
114 | return 1
115 | }, { prefix: '' })
116 | log.push(value)
117 | })
118 | f()
119 | t.deepEqual(log, [0, 1])
120 |
121 | f()
122 | t.deepEqual(log, [0, 1, 1])
123 |
124 | f.unhook()
125 | await frame(4)
126 | localStorage.removeItem('count')
127 |
128 | t.end()
129 | })
130 |
131 | t('useStorage: fn sometimes does not trigger', async t => {
132 | let log = []
133 | let f = enhook(() => {
134 | let [value] = useStorage('x', () => {
135 | log.push(0)
136 | return 1
137 | })
138 | log.push(value)
139 | })
140 | f()
141 | t.deepEqual(log, [0, 1])
142 |
143 | f.unhook()
144 | await frame(4)
145 |
146 | t.end()
147 | })
148 |
149 |
--------------------------------------------------------------------------------
/test/useChannel.js:
--------------------------------------------------------------------------------
1 | import { useChannel, useEffect } from '..'
2 | import t from 'tst'
3 | import enhook from './enhook.js'
4 | import { tick, frame, time } from 'wait-please'
5 |
6 | t('useChannel: does not init twice', async t => {
7 | let log = []
8 |
9 | let f1 = enhook(() => {
10 | let [value, setValue] = useChannel('a', 1)
11 | log.push(value)
12 | }), f2 = enhook(() => {
13 | let [value, setValue] = useChannel('a', 2)
14 | log.push(value)
15 | })
16 |
17 | f1()
18 | f2()
19 | await frame(2)
20 | t.deepEqual(log, [1, 1])
21 | await frame(2)
22 | t.deepEqual(log, [1, 1])
23 |
24 | f1.unhook()
25 | f2.unhook()
26 |
27 | await frame(2)
28 |
29 | t.end()
30 | })
31 |
32 | t('useChannel: does not trigger unchanged updates too many times', async t => {
33 | let log = []
34 | let fn = enhook((i) => {
35 | let [count, setCount] = useChannel('count', i)
36 |
37 | log.push(count)
38 | useEffect(() => {
39 | setCount(i + 1)
40 | setCount(i)
41 | }, [])
42 | })
43 | fn(1)
44 | t.deepEqual(log, [1])
45 | await frame(2)
46 | t.deepEqual(log, [1, 1])
47 |
48 | fn.unhook()
49 | await frame(2)
50 |
51 | t.end()
52 | })
53 |
54 | t('useChannel: does not call same-value update more than once', async t => {
55 | let log = []
56 | let f = enhook(() => {
57 | let [a, setA] = useChannel('count')
58 | log.push('a', a)
59 | let [b, setB] = useChannel('count', () => 1)
60 | log.push('b', b)
61 | let [c, setC] = useChannel('count', 2)
62 | log.push('c', c)
63 | useEffect(() => {
64 | setA(2)
65 | setB(2)
66 | setC(2)
67 | }, [])
68 | })
69 | f()
70 | t.deepEqual(log, ['a', undefined, 'b', 1, 'c', 1])
71 |
72 | log = []
73 | await frame(2)
74 | t.deepEqual(log, ['a', 2, 'b', 2, 'c', 2])
75 |
76 | f.unhook()
77 |
78 | t.end()
79 | })
80 |
81 | t('useChannel: reinitialize storage is fine', async t => {
82 | let log = []
83 |
84 | let f = enhook((key, value) => {
85 | let [foo] = useChannel(key, value)
86 | log.push(foo)
87 | })
88 | f('foo', 'bar')
89 | await frame()
90 | t.deepEqual(log, ['bar'])
91 | f('foo2', 'baz')
92 | await frame()
93 | t.deepEqual(log, ['bar', 'baz'])
94 |
95 | f.unhook()
96 |
97 | t.end()
98 | })
99 |
100 | t('useChannel: call init if key changes', async t => {
101 | let log = []
102 | let f = enhook((key) => {
103 | let [value, setValue] = useChannel(key, (oldValue) => {
104 | log.push('init', oldValue)
105 | return key
106 | })
107 | log.push('render', value)
108 | })
109 |
110 | f('x')
111 | t.deepEqual(log, ['init', undefined, 'render', 'x'])
112 | await frame(2)
113 | t.deepEqual(log, ['init', undefined, 'render', 'x'])
114 | await frame(2)
115 |
116 | f('x')
117 | t.deepEqual(log, ['init', undefined, 'render', 'x', 'render', 'x'])
118 | await frame(2)
119 |
120 | log = []
121 | f('y')
122 | t.deepEqual(log, ['init', undefined, 'render', 'y'])
123 | await frame(2)
124 | f('y')
125 | await frame(2)
126 | t.deepEqual(log, ['init', undefined, 'render', 'y', 'render', 'y'])
127 |
128 | f.unhook()
129 |
130 | t.end()
131 | })
132 |
133 | t('useChannel: async setter is fine', async t => {
134 | let log = []
135 |
136 | let f = enhook(() => {
137 | let [foo] = useChannel('foo', async () => {
138 | await tick()
139 | return 'foo'
140 | })
141 |
142 | log.push(foo)
143 | })
144 | f()
145 |
146 | t.ok(log[0] instanceof Promise)
147 | await time()
148 | t.equal(log[1], 'foo')
149 |
150 | f.unhook()
151 |
152 | t.end()
153 | })
154 |
155 | t('useStore: multiple components use same key', async t => {
156 | let log = []
157 |
158 | const f = (i) => {
159 | let [count, setCount] = useChannel('count', i)
160 | log.push('call', i, count)
161 | useEffect(() => {
162 | log.push('effect', i)
163 | setCount(i)
164 | }, [])
165 | }
166 | let f1 = enhook(f)
167 | let f2 = enhook(f)
168 |
169 | f1(1, log)
170 | t.deepEqual(log, ['call', 1, 1])
171 | await frame(4)
172 | t.deepEqual(log, ['call', 1, 1, 'effect', 1, 'call', 1, 1])
173 |
174 | log = []
175 | f2(2, log)
176 | await frame(4)
177 | t.deepEqualAny(log, [
178 | ['call', 2, 1, 'effect', 2, 'call', 2, 2, 'call', 1, 2],
179 | ['call', 2, 1, 'effect', 2, 'call', 1, 2, 'call', 2, 2],
180 | ])
181 |
182 | f1.unhook()
183 | f2.unhook()
184 | await frame(2)
185 |
186 | t.end()
187 | })
188 |
189 | t('useChannel: functional setter', async t => {
190 | let log = []
191 | let f = enhook(() => {
192 | let [count, setCount] = useChannel('count', 0)
193 | log.push(count)
194 | useEffect(() => {
195 | setCount(count => {
196 | return count + 1
197 | })
198 | }, [])
199 | })
200 | f()
201 | t.deepEqual(log, [0])
202 | await frame(2)
203 | t.deepEqual(log, [0, 1])
204 | f.unhook()
205 |
206 | t.end()
207 | })
208 |
209 | t('useChannel: init must be called once per key', async t => {
210 | let log = []
211 | let f = enhook(() => {
212 | let [a, setA] = useChannel('a', () => {
213 | log.push(1)
214 | })
215 | let [a1, setA1] = useChannel('a', () => {
216 | log.push(2)
217 | })
218 | })
219 | let f2 = enhook(() => {
220 | let [a, setA] = useChannel('a', () => {
221 | log.push(3)
222 | })
223 | })
224 | f()
225 | f2()
226 | t.deepEqual(log, [1])
227 | f.unhook()
228 | t.end()
229 | })
230 |
231 | t('useChannel: useQueryParam atomico case (must not fail)', async t => {
232 | let f1 = enhook(() => {
233 | let [a, set] = useChannel('a')
234 | })
235 | f1()
236 | f1.unhook()
237 |
238 | let f2 = enhook((params) => {
239 | let [a, set] = useChannel('a')
240 | set(1)
241 | })
242 | f2()
243 | f2.unhook()
244 |
245 | await frame(2)
246 |
247 | t.end()
248 | })
249 |
--------------------------------------------------------------------------------
/test/useStore.js:
--------------------------------------------------------------------------------
1 | import t from 'tst'
2 | import { useStore, useEffect, useState } from '../src/index'
3 | import { INTERVAL, storage, PREFIX, channels, createStore } from '../src/useStore'
4 | import enhook from './enhook.js'
5 | import { tick, idle, frame, time } from 'wait-please'
6 | import { clearNodeFolder } from 'broadcast-channel'
7 |
8 | t('useStore: debounce is 300ms', async t => {
9 | await clearNodeFolder()
10 | storage.set(PREFIX + 'count', undefined)
11 |
12 | let log = []
13 | let f = enhook(() => {
14 | let [count, setCount] = useStore('count', 0)
15 | log.push(count)
16 | useEffect(() => {
17 | setCount(1)
18 | }, [])
19 | })
20 | f()
21 | t.deepEqual(log, [0])
22 | await tick(2)
23 | // await time(INTERVAL)
24 | t.deepEqual(log, [0, 1])
25 |
26 | await teardown()
27 | t.end()
28 | })
29 |
30 | t('useStore: multiple components use same key', async t => {
31 | await clearNodeFolder()
32 | storage.set(PREFIX + 'count', null)
33 |
34 | let log = []
35 |
36 |
37 | const f = (i, log) => {
38 | let [count, setCount] = useStore('count', i)
39 | log.push('call', i, count)
40 | useEffect(() => {
41 | log.push('effect', i)
42 | setCount(i)
43 | }, [])
44 | }
45 | let f1 = enhook(f)
46 | let f2 = enhook(f)
47 |
48 | f1(1, log)
49 | t.deepEqual(log, ['call', 1, 1])
50 | await time(INTERVAL)
51 | t.deepEqual(log, ['call', 1, 1, 'effect', 1])
52 |
53 | f2(2, log)
54 | await time(INTERVAL * 2)
55 | t.deepEqual(log, ['call', 1, 1, 'effect', 1, 'call', 2, 1, 'effect', 2, 'call', 2, 2, 'call', 1, 2])
56 |
57 | await time(INTERVAL)
58 | await teardown()
59 | t.end()
60 | })
61 |
62 | t('useStore: does not trigger unchanged updates', async t => {
63 | await clearNodeFolder()
64 | storage.set(PREFIX + 'count', null)
65 |
66 | let log = []
67 | let fn = enhook((i) => {
68 | let [count, setCount] = useStore('count', i)
69 |
70 | log.push(count)
71 | useEffect(() => {
72 | setCount(i + 1)
73 | setCount(i)
74 | }, [])
75 | })
76 | fn(1)
77 | t.deepEqual(log, [1])
78 | await time(INTERVAL)
79 | t.deepEqual(log, [1])
80 |
81 | await time(INTERVAL * 2)
82 | await teardown()
83 | t.end()
84 | })
85 |
86 | t('useStore: fn init should be called per hook', async t => {
87 | await clearNodeFolder()
88 | storage.set(PREFIX + 'count', null)
89 |
90 | createStore('count', 0)
91 |
92 | let log = []
93 | let f = enhook(() => {
94 | useStore('count', -1)
95 | useStore('count', (count) => {
96 | log.push('init', count)
97 | return 1
98 | })
99 | let [value, setValue] = useStore('count', (count) => {
100 | log.push('reinit', count)
101 | return 2
102 | })
103 | log.push('call', value)
104 | })
105 | f()
106 | t.deepEqual(log, ['init', 0, 'reinit', 1, 'call', 2])
107 |
108 | log = []
109 | f()
110 | t.deepEqual(log, ['call', 2])
111 |
112 | await time(INTERVAL)
113 | await teardown()
114 |
115 | t.end()
116 | })
117 |
118 | t.skip('useStore: broadcast', async t => {
119 | // let React = await import('react')
120 | // let ReactDOM = await import('react-dom')
121 |
122 | // let el = document.createElement('div')
123 | // document.body.appendChild(el)
124 |
125 | // ReactDOM.render(, el)
126 |
127 | // function App() {
128 | // let [value, setValue] = useStore('foo', 0, { persist: true })
129 |
130 | // return
133 | // }
134 |
135 | t.end()
136 | })
137 |
138 |
139 |
140 | t('useStore: functional setter', async t => {
141 | await clearNodeFolder()
142 | storage.set(PREFIX + 'count', undefined)
143 |
144 | let log = []
145 | let f = enhook(() => {
146 | let [count, setCount] = useStore('count', 0)
147 | log.push(count)
148 | useEffect(() => {
149 | setCount(count => {
150 | return count + 1
151 | })
152 | }, [])
153 | })
154 | f()
155 | t.deepEqual(log, [0])
156 | await tick(2)
157 | // await time(INTERVAL)
158 | t.deepEqual(log, [0, 1])
159 |
160 | await teardown()
161 | t.end()
162 | })
163 |
164 | t('useStore: createStore must not rewrite existing data', async t => {
165 | await clearNodeFolder()
166 | storage.set(PREFIX + 'count', undefined)
167 |
168 | storage.set(PREFIX + 'xxx', {x: 1})
169 |
170 | let s = createStore('xxx', {y: 2})
171 | t.deepEqual(s, {x: 1})
172 |
173 | storage.set(PREFIX + 'xxx', null)
174 | await teardown()
175 | t.end()
176 | })
177 |
178 | t('useStore: persistence serializes store', async t => {
179 | await clearNodeFolder()
180 |
181 | let log = []
182 |
183 | let s = createStore('x', 1)
184 | let f1 = enhook(() => {
185 | let [x, setX] = useStore('x')
186 | setX(2)
187 | }, { passive: true })
188 | f1()
189 |
190 | await time(INTERVAL * 2)
191 | t.deepEqual(storage.get(PREFIX + 'x'), 2)
192 |
193 | storage.set(PREFIX + 'x', null)
194 | await teardown()
195 | t.end()
196 | })
197 |
198 | t('useStore: adjacent tabs do not cause recursion', async t => {
199 | // FIXME: spawn a copy in a separate tab
200 | await clearNodeFolder()
201 |
202 | let log = []
203 |
204 | let f1 = enhook(() => {
205 | let [x, setX] = useStore('x')
206 | useEffect(() => {
207 | setX({x: [1]})
208 | }, [])
209 | log.push(x)
210 | })
211 | f1()
212 |
213 | await time(INTERVAL * 6)
214 | t.deepEqual(log.filter(Boolean), [{x: [1]}])
215 |
216 | storage.set(PREFIX + 'x', null)
217 | await teardown()
218 | t.end()
219 | })
220 |
221 | t('useStore: removing components must not close channel', async t => {
222 | await clearNodeFolder()
223 |
224 | let log = []
225 |
226 | let f1 = enhook(() => {
227 | let [x, setX] = useStore('x')
228 | useEffect(() => setX(1), [])
229 | })
230 | f1()
231 | let f2 = enhook((v) => {
232 | let [x, setX] = useStore('x')
233 | useEffect(() => setX(v), [v])
234 | })
235 |
236 | await frame()
237 | f1.unhook()
238 | await frame()
239 |
240 | f2(2)
241 | await frame()
242 | f2.unhook()
243 | await frame()
244 |
245 | await teardown()
246 | t.end()
247 | })
248 |
249 |
250 | export async function teardown() {
251 | storage.set(PREFIX + 'count', null)
252 | for (let channel in channels) { (channels[channel].close(), delete channels[channel]) }
253 | }
254 |
--------------------------------------------------------------------------------
/test/useSearchParam.js:
--------------------------------------------------------------------------------
1 | import t from 'tst'
2 | import { useSearchParam } from '../'
3 | import enhook from './enhook.js'
4 | import { tick, idle, frame, time } from 'wait-please'
5 |
6 | t.browser('useSearchParam: read values properly', async t => {
7 | clearSearch()
8 | let log = []
9 |
10 | let str = new URLSearchParams({
11 | str: 'foo',
12 | num: -123,
13 | bool: false,
14 | nul: null,
15 | undef: undefined,
16 | arr1: [1, 2, 3],
17 | arr2: ['a', 'b', 'c'],
18 | arr3: [['a', 1], ['b', 2], ['c', 3]],
19 | arr4: [
20 | { a: 1, toString() {return 'a:' + this.a} },
21 | { b: 2, toString() { return 'b:' + this.b } },
22 | { c: 3, toString() { return 'c:' + this.c } }
23 | ],
24 | obj: { a: 1, b: 2, c: 3, foo: 'bar', toString(){return JSON.stringify(this)} },
25 | date: +new Date('2019-11-17')
26 | }).toString()
27 | window.history.pushState(null, '', '?' + str)
28 |
29 | let f = enhook(() => {
30 | let [str, setStr] = useSearchParam('str')
31 | let [num, setNum] = useSearchParam('num')
32 | let [bool, setBool] = useSearchParam('bool')
33 | let [nul, setNul] = useSearchParam('nul')
34 | let [undef, setUndef] = useSearchParam('undefined')
35 | let [arr1, setArr1] = useSearchParam('arr1')
36 | let [arr2, setArr2] = useSearchParam('arr2')
37 | let [arr3, setArr3] = useSearchParam('arr3')
38 | let [arr4, setArr4] = useSearchParam('arr4')
39 | let [obj, setObj] = useSearchParam('obj')
40 | let [date, setDate] = useSearchParam('date')
41 |
42 | log.push(str)
43 | log.push(num)
44 | log.push(bool)
45 | log.push(nul)
46 | log.push(undef)
47 | log.push(arr1)
48 | log.push(arr2)
49 | log.push(arr3)
50 | log.push(arr4)
51 | log.push(obj)
52 | log.push(date)
53 | })
54 |
55 | f()
56 | t.deepEqual(log, [
57 | 'foo',
58 | '-123',
59 | 'false',
60 | 'null',
61 | undefined,
62 | '1,2,3',
63 | 'a,b,c',
64 | 'a,1,b,2,c,3',
65 | 'a:1,b:2,c:3',
66 | '{"a":1,"b":2,"c":3,"foo":"bar"}',
67 | +new Date('2019-11-17T00:00:00.000Z') + ''
68 | ])
69 |
70 | f.unhook()
71 | clearSearch()
72 | await frame(5)
73 | t.end()
74 | })
75 |
76 | t.browser('useSearchParam: write values', async t => {
77 | clearSearch()
78 |
79 | let f = enhook((params) => {
80 | let [str, setStr] = useSearchParam('str')
81 | let [num, setNum] = useSearchParam('num')
82 | let [bool, setBool] = useSearchParam('bool')
83 | let [arr1, setArr1] = useSearchParam('arr1')
84 | let [arr2, setArr2] = useSearchParam('arr2')
85 | let [arr3, setArr3] = useSearchParam('arr3')
86 | let [arr4, setArr4] = useSearchParam('arr4')
87 | let [obj, setObj] = useSearchParam('obj')
88 | let [date, setDate] = useSearchParam('date')
89 |
90 | setStr(params.str)
91 | setNum(params.num)
92 | setBool(params.bool)
93 | setArr1(params.arr1)
94 | setArr2(params.arr2)
95 | setArr3(params.arr3)
96 | setArr4(params.arr4)
97 | setObj(params.obj)
98 | setDate(params.date)
99 | })
100 |
101 | f({
102 | str: 'foo',
103 | num: -124,
104 | bool: false,
105 | arr1: [1, 2, 3],
106 | arr2: ['a', 'b', 'c'],
107 | arr3: [['a', 1], ['b', 2], ['c', 3]],
108 | arr4: [
109 | { a: 1, toString() { return 'a:' + this.a } },
110 | { b: 2, toString() { return 'b:' + this.b } },
111 | { c: 3, toString() { return 'c:' + this.c } }
112 | ],
113 | obj: { a: 1, b: 2, c: 3, foo: 'bar', toString(){return JSON.stringify(this)} },
114 | date: new Date('2019-11-17').toISOString()
115 | })
116 | await frame(2)
117 | let params = new URLSearchParams(window.location.search.slice(1))
118 | t.is(params.get("arr1"), "1,2,3")
119 | t.is(params.get("arr2"), "a,b,c")
120 | t.is(params.get("arr3"), "a,1,b,2,c,3")
121 | t.is(params.get("arr4"), "a:1,b:2,c:3")
122 | t.is(params.get("bool"), "false")
123 | t.is(params.get("date"), "2019-11-17T00:00:00.000Z")
124 | t.is(params.get("num"), "-124")
125 | t.is(params.get("obj"), JSON.stringify({ "a": 1, "b": 2, "c": 3, "foo": "bar" }))
126 | t.is(params.get("str"), "foo")
127 |
128 | await frame(3)
129 | f.unhook()
130 | // clearSearch()
131 | await frame(3)
132 | t.end()
133 | })
134 |
135 | t.browser('useSearchParam: defaults', async t => {
136 | let log = []
137 |
138 | clearSearch()
139 | await time(100)
140 |
141 | let f = enhook(() => {
142 | let [str, setStr] = useSearchParam('str', 'foo')
143 | let [num, setNum] = useSearchParam('num', '-123')
144 | let [bool, setBool] = useSearchParam('bool', 'false')
145 |
146 | log.push(str)
147 | log.push(num)
148 | log.push(bool)
149 | })
150 |
151 | f()
152 | t.deepEqual(log, [
153 | 'foo',
154 | '-123',
155 | 'false'
156 | ])
157 | await tick()
158 |
159 | let params = new URLSearchParams(window.location.search.slice(1))
160 |
161 | t.equal(params.get("bool"), "false")
162 | t.equal(params.get("num"), "-123")
163 | t.equal(params.get("str"), "foo")
164 |
165 |
166 | f.unhook()
167 | await time(50)
168 | // clearSearch()
169 | t.end()
170 | })
171 |
172 | t.browser('useSearchParam: observe updates', async t => {
173 | clearSearch()
174 | let log = []
175 | let f = enhook(() => {
176 | let [v, setV] = useSearchParam('x', 1)
177 | log.push(v)
178 | })
179 | f()
180 | await frame(2)
181 | t.deepEqual(log, [1])
182 |
183 | window.history.pushState(null, "useSearchParam", "?x=2");
184 | await frame()
185 | t.deepEqual(log, [1, '2'])
186 |
187 | window.history.pushState(null, "useSearchParam", "?x=3");
188 | await frame(3)
189 | t.deepEqual(log, [1, '2', '3'])
190 |
191 | f.unhook()
192 | clearSearch()
193 | t.end()
194 | })
195 |
196 | t.browser('useSearchParam: default array', async t => {
197 | let f = enhook(() => {
198 | let [arr, setArr] = useSearchParam('arr', [])
199 | t.deepEqual(arr, [])
200 | })
201 | f()
202 |
203 | await frame(2)
204 | f.unhook()
205 | clearSearch()
206 | t.end()
207 | })
208 |
209 | t.browser('useSearchParam: init from empty string', async t => {
210 | let log = []
211 |
212 | window.history.pushState(null, '', '?x=1')
213 | let f = enhook(() => {
214 | let [x] = useSearchParam('x', '')
215 | log.push(x)
216 | })
217 | f()
218 | t.deepEqual(log, ['1'])
219 |
220 | f.unhook()
221 |
222 | clearSearch()
223 | t.end()
224 | })
225 |
226 | function clearSearch() {
227 | window.history.pushState(null, '', window.location.href.split('?')[0])
228 | }
229 |
--------------------------------------------------------------------------------
/test/useFormField.js:
--------------------------------------------------------------------------------
1 | import t from 'tst'
2 | import { useFormField, useEffect, useState } from '../'
3 | import { render } from 'preact'
4 | import { html } from 'htm/preact'
5 | import { tick, time, frame } from 'wait-please'
6 |
7 | t('useFormField: should control existing input via actions', async t => {
8 | let el = document.createElement('div')
9 | // document.body.appendChild(el)
10 |
11 | let log = []
12 |
13 | let Comp = () => {
14 | let field = useFormField({name: 'x', value: 1})
15 | useEffect(() => {
16 | log.push(field.value, field.error, field.touched)
17 | field.set(2)
18 | setTimeout(() => {
19 | log.push(field.value, field.error, field.touched)
20 | field.set(null)
21 | }, 10)
22 | setTimeout(() => {
23 | log.push(field.value, field.error, field.touched)
24 | field.reset()
25 | }, 20)
26 | setTimeout(() => {
27 | log.push(field.value, field.error, field.touched)
28 | }, 30);
29 | }, [])
30 | return html``
31 | }
32 |
33 | render(html`<${Comp}/>`, el)
34 |
35 | await time(50)
36 | t.deepEqual(log, [
37 | 1, null, false,
38 | 2, null, false,
39 | null, null, false,
40 | 1, null, false
41 | ])
42 |
43 | // document.body.removeChild(el)
44 | render(null, el)
45 |
46 | t.end()
47 | })
48 |
49 | t('useFormField: state should reflect interactions', async t => {
50 | let el = document.createElement('div')
51 | // document.body.appendChild(el)
52 |
53 | let log = []
54 | let Comp = () => {
55 | let [props,field] = useFormField()
56 | log.push(field.value, field.touched)
57 | return html``
58 | }
59 | render(html`<${Comp}/>`, el)
60 |
61 | await frame()
62 |
63 | let input = el.querySelector('input')
64 | input.dispatchEvent(new Event('focus'))
65 | input.value = 'a'
66 | input.dispatchEvent(new InputEvent('input', { data: 'a'}))
67 |
68 | await frame(2)
69 | t.deepEqual(log, ['', false, 'a', true])
70 |
71 | // document.body.removeChild(el)
72 | render(null, el)
73 |
74 | t.end()
75 | })
76 |
77 | t('useFormField: should be able to set value', async t => {
78 | let el = document.createElement('div')
79 | // document.body.appendChild(el)
80 |
81 | let log = []
82 | let Comp = () => {
83 | let field = useFormField({value: 'foo'})
84 | useEffect(() => {
85 | field.set('bar')
86 | }, [])
87 | return html``
88 | }
89 | render(html`<${Comp}/>`, el)
90 |
91 | let input = el.querySelector('input')
92 | t.deepEqual(input.value, 'foo')
93 | await frame(2)
94 | t.deepEqual(input.value, 'bar')
95 |
96 | // document.body.removeChild(el)
97 | render(null, el)
98 |
99 | t.end()
100 | })
101 |
102 | t('useFormField: should be able to validate value', async t => {
103 | let el = document.createElement('div')
104 | // document.body.appendChild(el)
105 |
106 | let log = []
107 | let Comp = () => {
108 | let field = useFormField({ validate: value => !!value })
109 | log.push(field.error, field.valid)
110 | return html``
111 | }
112 | render(html`<${Comp}/>`, el)
113 |
114 | let input = el.querySelector('input')
115 | input.dispatchEvent(new Event('focus'))
116 | input.value = ''
117 | input.dispatchEvent(new Event('input', { data: '' }))
118 | input.dispatchEvent(new Event('blur'))
119 |
120 | t.deepEqual(log, [null, true])
121 | await frame(2)
122 | t.deepEqual(log, [null, true, false, false])
123 |
124 | input.dispatchEvent(new Event('focus'))
125 | input.value = 'foo'
126 | input.dispatchEvent(new Event('input', { data: 'foo' }))
127 | input.dispatchEvent(new Event('blur'))
128 |
129 | await frame(2)
130 | t.deepEqual(log, [null, true, false, false, null, true])
131 |
132 | // document.body.removeChild(el)
133 | render(null, el)
134 |
135 | t.end()
136 | })
137 |
138 | t('useFormField: does not crash on null-validation', async t => {
139 | let el = document.createElement('div')
140 | // document.body.appendChild(el)
141 |
142 | let log = []
143 | let Comp = () => {
144 | let field = useFormField()
145 | log.push(field.error)
146 | return html``
147 | }
148 | render(html`<${Comp}/>`, el)
149 |
150 | let input = el.querySelector('input')
151 | input.dispatchEvent(new Event('focus'))
152 | input.value = ''
153 | input.dispatchEvent(new Event('input', { data: '' }))
154 | input.dispatchEvent(new Event('blur'))
155 |
156 | t.deepEqual(log, [null])
157 | await frame(2)
158 | t.deepEqual(log, [null, null])
159 |
160 | input.dispatchEvent(new Event('focus'))
161 | input.value = 'foo'
162 | input.dispatchEvent(new Event('input', { data: 'foo' }))
163 | input.dispatchEvent(new Event('blur'))
164 |
165 | await frame(2)
166 | t.deepEqual(log, [null, null, null])
167 |
168 | // document.body.removeChild(el)
169 | render(null, el)
170 |
171 | t.end()
172 | })
173 |
174 | t.skip('useFormField: persist test', async t => {
175 | let el = document.createElement('div')
176 | document.body.appendChild(el)
177 |
178 | let Comp = () => {
179 | let field = useFormField({ persist: true })
180 | return html``
181 | }
182 | render(html`<${Comp}/>`, el)
183 |
184 | await frame(2)
185 |
186 | // document.body.removeChild(el)
187 | // render(null, el)
188 |
189 | t.end()
190 | })
191 |
192 | t('useFormField: focus must reflect focused state', async t => {
193 | let el = document.createElement('div')
194 |
195 | let log = []
196 | render(html`<${function () {
197 | let field = useFormField('')
198 | log.push(field.focus)
199 | return html``
200 | }}/>`, el)
201 | let input = el.querySelector('input')
202 | await frame(1)
203 | input.dispatchEvent(new Event('focus'))
204 | await frame(1)
205 | input.dispatchEvent(new Event('blur'))
206 | await frame(1)
207 |
208 | t.deepEqual(log, [false, true, false])
209 |
210 | render(null, el)
211 | t.end()
212 | })
213 |
214 | t('useFormField: should not be validated if focused', async t => {
215 | let el = document.createElement('div')
216 | // document.body.appendChild(el)
217 |
218 | let log = []
219 | render(html`
220 | <${function () {
221 | let field = useFormField({validate(value) { return value ? 'Valid' : 'Invalid' }})
222 | log.push(field.error)
223 | return html` ${ field.error + '' }`
224 | }}/>
225 | `, el)
226 |
227 | let input = el.querySelector('input')
228 | input.dispatchEvent(new Event('focus'))
229 | input.value = 'a'
230 | input.dispatchEvent(new InputEvent('input'))
231 |
232 | await frame(2)
233 | // t.deepEqual(log, [null, 'Valid'])
234 | t.deepEqual(log, [null, null])
235 |
236 | input.value = ''
237 | input.dispatchEvent(new InputEvent('input'))
238 |
239 | await frame(2)
240 | t.deepEqual(log, [null, null, null])
241 |
242 | input.dispatchEvent(new Event('blur'))
243 | await frame(2)
244 | t.deepEqual(log, [null, null, null, 'Invalid'])
245 |
246 | render(null, el)
247 |
248 | t.end()
249 | })
250 |
251 | t('useFormField: `required` should turn initial valid state into false', async t => {
252 | let el = document.createElement('div')
253 | // document.body.appendChild(el)
254 |
255 | let log = []
256 | render(html`
257 | <${function () {
258 | let field = useFormField({ required: true })
259 | log.push(field.valid)
260 | return html` ${field.error + ''}`
261 | }}/>
262 | `, el)
263 | await frame(2)
264 |
265 | let input = el.querySelector('input')
266 | t.deepEqual(log, [true])
267 |
268 | input.dispatchEvent(new Event('focus'))
269 | input.value = 'a'
270 | input.dispatchEvent(new InputEvent('input'))
271 | await frame(2)
272 | t.deepEqual(log, [true, true])
273 |
274 | input.value = ''
275 | input.dispatchEvent(new InputEvent('input'))
276 | input.dispatchEvent(new Event('blur'))
277 |
278 | await frame(2)
279 | t.deepEqual(log, [true, true, false])
280 | render(null, el)
281 |
282 | t.end()
283 | })
284 |
285 | t('useFormField: changed input props must be updated', async t => {
286 | let el = document.createElement('div')
287 | let log = []
288 |
289 | render(html`<${function () {
290 | let [x, setX] = useState([1])
291 | let field = useFormField({ x })
292 | log.push(field.x)
293 | useEffect(() => setX([2]), [])
294 |
295 | return null
296 | }}/>`, el)
297 |
298 | await frame(3)
299 | t.deepEqual(log, [[1], [2]])
300 |
301 | render(null, el)
302 |
303 | t.end()
304 | })
305 |
306 | t.todo('useFormField: initial value should not be null')
307 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import setHooks, * as hooks from "any-hooks"
2 |
3 | export * from 'any-hooks'
4 | export default setHooks
5 |
6 | const listeners = globalThis.__uhxListeners || (globalThis.__uhxListeners = {}),
7 | values = globalThis.__uhxValues || (globalThis.__uhxValues = {})
8 |
9 | export function useChannel(key, init, deps=[]) {
10 | let [, setState] = hooks.useState()
11 | let initKey = hooks.useRef()
12 |
13 | hooks.useMemo(() => {
14 | (listeners[key] || (listeners[key] = [])).push(setState)
15 |
16 | if (init === undefined) return
17 | // if (key in values) return console.warn(`Channel ${key} is already initialized.`)
18 | if (
19 | !(key in values) || // init run
20 | initKey.current === key // or deps changed
21 | ) {
22 | initKey.current = key
23 | if (typeof init === 'function') init = init()
24 | values[key] = init
25 | if (init && init.then) init.then(init => setState(values[key] = init))
26 | }
27 | }, [key, ...deps])
28 |
29 | hooks.useLayoutEffect(() => () => {
30 | listeners[key].splice(listeners[key].indexOf(setState) >>> 0, 1)
31 | if (!listeners[key].length) {
32 | delete values[key]
33 | }
34 | }, [key, ...deps])
35 |
36 | return [values[key], (value) => {
37 | values[key] = typeof value === 'function' ? value(values[key]) : value
38 | listeners[key].map((setState) => setState(value))
39 | }]
40 | }
41 |
42 | export function useAction(key, init, deps = []) {
43 | let [action, setAction] = useChannel('__uhx:action-' + key, init !== undefined ? () => init : undefined, deps)
44 | return hooks.useMemo(() => {
45 | if (typeof action !== 'function') return [action]
46 | action[Symbol.iterator] = function*() { yield action; yield setAction; }
47 | return action
48 | }, [action, ...deps])
49 | }
50 |
51 | export function useStorage(key, init, o) {
52 | o = { storage: window.localStorage, prefix: '__uhx:storage-', ...(o || {}) }
53 | let storeKey = o.prefix + key
54 | let [value, setValue] = useChannel(key, () => {
55 | // init from stored value, if any
56 | let stored = o.storage.getItem(storeKey)
57 | if (stored != null) stored = JSON.parse(stored)
58 | if (typeof init === 'function') return init(stored)
59 | return stored == null ? init : stored
60 | })
61 |
62 | hooks.useMemo(() => {
63 | // persist initial value, if store is rewired
64 | if (init === undefined) return
65 | if (o.storage.getItem(storeKey) == null) o.storage.setItem(storeKey, JSON.stringify(value))
66 | }, [key])
67 |
68 | hooks.useEffect(() => {
69 | // notify listeners, subscribe to storage changes
70 | let update = e => {
71 | if (e.key !== storeKey) return
72 | setValue(e.newValue)
73 | }
74 | window.addEventListener('storage', update)
75 | return () => window.removeEventListener('storage', update)
76 | }, [key])
77 |
78 | return [value, (value) => {
79 | setValue(value)
80 | // side-effect write
81 | o.storage.setItem(storeKey, JSON.stringify(value))
82 | }]
83 | }
84 |
85 | wrapHistory('push')
86 | wrapHistory('replace')
87 | enableNavigateEvent()
88 | export function useSearchParam(key, init) {
89 | let [value, setValue] = useChannel('__uhx:search-param-' + key, () => {
90 | let params = new URLSearchParams(window.location.search)
91 | let param
92 | if (params.has(key)) param = params.get(key)
93 | if (typeof init === 'function') return init(param)
94 | return param == null ? init : param
95 | })
96 |
97 | hooks.useMemo(() => {
98 | if (init === undefined) return
99 | let params = new URLSearchParams(window.location.search)
100 | if (!params.has(key)) {
101 | params.set(key, value)
102 | setURLSearchParams(params)
103 | }
104 | }, [key])
105 |
106 | hooks.useEffect(() => {
107 | const update = (e) => {
108 | let params = new URLSearchParams(window.location.search)
109 | let newValue = params.get(key)
110 | // FIXME: there's a test case failing here from wishbox - lang didn't switch more than once.
111 | // if (newValue !== value) setValue(newValue)
112 | setValue(newValue)
113 | }
114 |
115 | window.addEventListener('popstate', update)
116 | window.addEventListener('pushstate', update)
117 | window.addEventListener('replacestate', update)
118 | window.addEventListener('navigate', update)
119 |
120 | return () => {
121 | window.removeEventListener('popstate', update)
122 | window.removeEventListener('pushstate', update)
123 | window.removeEventListener('replacestate', update)
124 | window.removeEventListener('navigate', update)
125 | }
126 | }, [key])
127 |
128 | return [value, value => {
129 | let params = new URLSearchParams(window.location.search)
130 | params.set(key, value)
131 | setURLSearchParams(params)
132 | }]
133 | }
134 | function setURLSearchParams(params) {
135 | let str = params.toString()
136 | window.history.replaceState(null, '', str ? '?' + str : window.location.href.split('?')[0])
137 | }
138 | // https://stackoverflow.com/a/25673946/1052640
139 | // https://github.com/lukeed/navaid/blob/master/src/index.js#L80-L90
140 | function wrapHistory(type) {
141 | type += 'State'
142 | const fn = history[type]
143 | history[type] = function (uri) {
144 | let result = fn.apply(this, arguments)
145 | let ev = new Event(type.toLowerCase())
146 | ev.uri = uri
147 | ev.arguments = arguments
148 | window.dispatchEvent(ev)
149 | return result
150 | }
151 | return () => {
152 | history[type] = fn
153 | }
154 | }
155 | // https://github.com/WebReflection/onpushstate
156 | // https://github.com/lukeed/navaid/blob/master/src/index.js#L52-L60
157 | function enableNavigateEvent() {
158 | const handleNavigate = (e) => {
159 | // find the link node (even if inside an opened Shadow DOM)
160 | var target = e.target.shadowRoot ? e.path[0] : e.target;
161 | // find the anchor
162 | var anchor = target.closest('A')
163 |
164 | // if found
165 | if (!anchor) return
166 |
167 | // it's not a click with ctrl/shift/alt keys pressed
168 | // => (let the browser do it's job instead)
169 | if (!e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey && !e.button && !e.defaultPrevented) return
170 |
171 | // it's for the current page
172 | if (!/^(?:_self)?$/i.test(anchor.target)) return
173 |
174 | // same origin
175 | if (anchor.host !== location.host) return
176 |
177 | // it's not a download
178 | if (anchor.hasAttribute('download')) return
179 |
180 | // it's not a resource handled externally
181 | if (anchor.getAttribute('rel') === 'external') return
182 |
183 | // let empty links be (see issue #5)
184 | if (!anchor.href) return
185 |
186 | var e = new Event('navigate');
187 | window.dispatchEvent(e);
188 | }
189 | document.addEventListener('click', handleNavigate, true)
190 | return () => document.removeEventListener('click', handleNavigate)
191 | }
192 |
193 | export function useCountdown(n, interval = 1000) {
194 | const [count, set] = hooks.useState(n)
195 |
196 | const reset = hooks.useCallback(() => set(n), [n])
197 |
198 | const schedule = hooks.useMemo(() => {
199 | return typeof interval === 'function' ? interval : fn => {
200 | let id = setInterval(fn, interval)
201 | return () => clearInterval(id)
202 | }
203 | }, [interval])
204 |
205 | hooks.useEffect(() => {
206 | const unschedule = schedule(() => {
207 | set(count => {
208 | if (count <= 0) return (unschedule(), 0)
209 | else return count - 1
210 | })
211 | })
212 |
213 | return unschedule
214 | }, [n, schedule])
215 |
216 | return [count, reset]
217 | }
218 |
219 | export function usePrevious(value) {
220 | let ref = hooks.useRef()
221 |
222 | hooks.useEffect(() => {
223 | ref.current = value
224 | }, [value])
225 |
226 | return ref.current
227 | }
228 |
229 | export function useUpdate() {
230 | let [, set] = hooks.useState(0)
231 | let update = hooks.useCallback(() => set(s => ~s), [])
232 | return update
233 | }
234 |
235 | export function useValidate(validator, init) {
236 | let check = hooks.useCallback((value) => {
237 | if (!validator) return null
238 |
239 | let check = (value, validator) => {
240 | try {
241 | var valid = validator(value)
242 | if (valid === true || valid == null) {
243 | return null
244 | }
245 | throw valid
246 | } catch (e) {
247 | return e
248 | }
249 | }
250 |
251 | if (Array.isArray(validator)) {
252 | for (let i = 0, error; i < validator.length; i++) {
253 | error = check(value, validator[i])
254 | if (error !== null) return error
255 | }
256 | return null
257 | }
258 |
259 | return check(value, validator)
260 | }, [])
261 |
262 |
263 | let [error, setError] = hooks.useState(init != null ? () => check(init) : null)
264 |
265 | let validate = hooks.useCallback((value) => {
266 | let error = check(value, validator)
267 | setError(error)
268 | return error == null
269 | }, [])
270 |
271 | return [error, validate]
272 | }
273 |
274 | export function useFormField(props = {}) {
275 | const prefix = '__uhx:form-field-'
276 |
277 | let { value='', validate: rules, required, persist, ...inputProps } = props
278 | let key = inputProps.name || inputProps.id || inputProps.key
279 |
280 | let inputRef = hooks.useRef()
281 | let [init, setValue] = persist ? useStorage(key, value, { storage: window.sessionStorage, prefix }) : useChannel(key, value)
282 | let [focus, setFocus] = hooks.useState(false)
283 | let [error, validate] = useValidate(rules || (required ? v => !!v : null))
284 |
285 | let field = hooks.useMemo(() => {
286 | let getValue = () => field.value
287 |
288 | let field = Object.create(
289 | // invisible, not enumerable (interface)
290 | {
291 | valueOf: getValue,
292 | [Symbol.toPrimitive]: getValue,
293 | [Symbol.iterator]: function* () {
294 | yield { ...field } // leave only enumerables (input props)
295 | yield field
296 | }
297 | }, {
298 | // invisible, enumerable (input props)
299 | // FIXME: there's no way to define invisible + enumerable for spread
300 | onBlur: {
301 | enumerable: true, value: e => {
302 | setFocus(field.focus = false)
303 | // revert error to the last value
304 | field.validate()
305 | }
306 | },
307 | onFocus: {
308 | enumerable: true, value: e => {
309 | field.touched = true
310 | field.error = null
311 | field.valid = true
312 | setFocus(field.focus = true)
313 | }
314 | },
315 | onChange: { enumerable: true, value: e => field.set(e.target.value) },
316 | onInput: { enumerable: true, value: e => field.set(e.target.value) },
317 |
318 | // visible, not enumerable (field state)
319 | error: { enumerable: false, writable: true, value: null },
320 | valid: { enumerable: false, writable: true, value: true },
321 | focus: { enumerable: false, writable: true, value: false },
322 | touched: { enumerable: false, writable: true, value: false },
323 | set: {
324 | enumerable: false, value: (v) => {
325 | setValue(field.value = v)
326 | if (!field.focus) field.validate()
327 | }
328 | },
329 | reset: {
330 | enumerable: false, value: () => {
331 | setValue(field.value = init)
332 | field.error = null
333 | field.valid = true
334 | field.touched = false
335 | }
336 | },
337 | validate: {
338 | enumerable: false, value: (value = field.value) => field.valid = validate(value)
339 | }
340 | })
341 |
342 | // visible, enumerable (field state + input props)
343 | Object.assign(field, {
344 | value: init,
345 | required,
346 | ref: inputRef,
347 | })
348 |
349 | return field
350 | }, [])
351 |
352 | // sync error with useValidate, recover from focus as well
353 | hooks.useMemo(() => {
354 | if (!field.focus) field.error = error
355 | }, [error, field.focus])
356 |
357 | // update input props whenever they change
358 | hooks.useMemo(() => {
359 | Object.assign(field, inputProps)
360 | }, Object.keys(inputProps).map(key => inputProps[key]))
361 |
362 | return field
363 | }
364 |
365 | export function useInput(ref, init) {
366 | if (ref.nodeType) ref = { current: ref }
367 |
368 | let key = '__uhx:input-' + (ref.current && `${ref.current.id || ''}:${ref.current.type || 'text'}:${ref.current.name || ''}`)
369 |
370 | let [value, setValue] = useChannel(key, () => {
371 | // init from input
372 | let value = ref.current && ref.current.value
373 | if (value != null) {
374 | if (typeof init === 'function') return init(value)
375 | return value
376 | }
377 | return init
378 | })
379 |
380 | hooks.useMemo(() => {
381 | // write value if input is rewired
382 | if (init === undefined) return
383 | if (ref.current && ref.current.value == null) {
384 | ref.current.setAttribute('value', ref.current.value = value)
385 | if (value == null) ref.current.removeAttribute(value)
386 | }
387 | }, [ref.current])
388 |
389 | hooks.useEffect(() => {
390 | // notify listeners, subscribe to input changes
391 | let update = e => setValue(e.target.value)
392 | ref.current.addEventListener('input', update)
393 | ref.current.addEventListener('change', update)
394 | return () => {
395 | ref.current.removeEventListener('input', update)
396 | ref.current.removeEventListener('change', update)
397 | }
398 | }, [ref.current])
399 |
400 | return [value, (value) => {
401 | setValue(value)
402 | // main write to input
403 | ref.current.setAttribute('value', ref.current.value = value)
404 | if (value == null) ref.current.removeAttribute('value')
405 | }]
406 | }
407 |
408 | export function useObservable(v) {
409 | let [state, setState] = hooks.useState(v())
410 | hooks.useEffect(() => v(v => setState(v)), [])
411 | return [state, state => v(state)]
412 | }
413 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # unihooks  [](https://travis-ci.org/unihooks/unihooks)
2 |
3 | Essential hooks collection for everyday react[1](#user-content-1) projects.
4 |
5 | [](https://nodei.co/npm/unihooks/)
6 |
7 |
20 |
21 | ## Principles
22 |
23 |
24 | 1. Framework agnostic
25 |
26 |
27 | _Unihooks_ are not bound to react and work with any hooks-enabled framework:
28 |
29 | * [react](https://ghub.io/react)
30 | * [preact](https://ghub.io/preact)
31 | * [haunted](https://ghub.io/haunted)
32 | * [neverland](https://ghub.io/neverland)
33 | * [atomico](https://ghub.io/atomico)
34 | * [fuco](https://ghub.io/fuco)
35 | * [spect](https://ghub.io/spect)
36 | * [augmentor](https://ghub.io/augmentor)
37 |
38 | See [any-hooks](https://ghub.io/any-hooks) for the full list.
39 |
40 |
50 |
51 |
52 |
53 | 2. Unified
54 |
55 |
56 | _Unihooks_ follow `useState` signature for intuitivity.
57 |
58 | ```js
59 | let [ state, actions ] = useValue( target?, init | update? )
60 | ```
61 |
62 |
65 |
66 |
67 |
68 |
69 | 3. Essential
70 |
71 |
72 | _Unihooks_ deliver value in reactive context, they're not mere wrappers for native API. Static hooks are avoided.
73 |
74 | ```js
75 | const MyComponent = () => { let ua = useUserAgent() } // ✘ − user agent never changes
76 | const MyComponent = () => { let ua = navigator.userAgent } // ✔ − direct API must be used instead
77 | ```
78 |
79 |
85 |
86 |
87 | ## Hooks
88 |
89 |
90 | useChannel
91 |
92 | #### `[value, setValue] = useChannel(key, init?, deps?)`
93 |
94 | Global value provider - `useState` with value identified globally by `key`.
95 | Can be used as value store, eg. as application model layer without persistency. Also can be used for intercomponent communication.
96 |
97 | `init` can be a value or a function, and (re)applies if the `key` (or `deps`) changes.
98 |
99 | ```js
100 | import { useChannel } from 'unihooks'
101 |
102 | function Component () {
103 | let [users, setUsers] = useChannel('users', {
104 | data: [],
105 | loading: false,
106 | current: null
107 | })
108 |
109 | setUsers({ ...users, loading: true })
110 |
111 | // or as reducer
112 | setUsers(users => { ...users, loading: false })
113 | }
114 | ```
115 |
116 |
117 |
118 |
119 | useStorage
120 |
121 | #### `[value, setValue] = useStorage(key, init?, options?)`
122 |
123 | `useChannel` with persistency to local/session storage. Subscribes to `storage` event - updates if storage is changed from another tab.
124 |
125 | ```js
126 | import { useStorage } from 'unihooks'
127 |
128 | function Component1 () {
129 | const [count, setCount] = useStorage('my-count', 1)
130 | }
131 |
132 | function Component2 () {
133 | const [count, setCount] = useStorage('my-count')
134 | // count === 1
135 |
136 | setCount(2)
137 | // (↑ updates Component1 too)
138 | }
139 |
140 | function Component3 () {
141 | const [count, setCount] = useStorage('another-count', (value) => {
142 | // ...initialize value from store
143 | return value
144 | })
145 | }
146 | ```
147 |
148 | #### `options`
149 |
150 | * `prefix` - prefix that's added to stored keys.
151 | * `storage` - manually pass session/local/etc storage.
152 |
153 |
154 | Reference: [useStore](https://ghub.io/use-store).
155 |
156 |
157 |
158 |
159 | useAction
160 |
161 | #### `[action] = useAction(key, cb, deps?)`
162 |
163 | Similar to `useChannel`, but used for storing functions. Different from `useChannel` in the same way the `useCallback` is different from `useMemo`. `deps` indicate if value must be reinitialized.
164 |
165 | ```js
166 | function RootComponent() {
167 | useAction('load-content', async (slug, fresh = false) => {
168 | const url = `/content/${slug}`
169 | const cache = fresh ? 'reload' : 'force-cache'
170 | const res = await fetch(url, { cache })
171 | return await res.text()
172 | })
173 | }
174 |
175 | function Content ({ slug = '/' }) {
176 | let [content, setContent] = useState()
177 | let [load] = useAction('load-content')
178 | useEffect(() => load().then(setContent), [slug])
179 | return html`
180 | ${content}
181 | `
182 | }
183 | ```
184 |
185 |
186 |
187 |
188 | useSearchParam
189 |
190 | #### `[value, setValue] = useSearchParam(name, init?)`
191 |
192 | Reflect value to `location.search`. `value` is turned to string via [URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams).
193 | To serialize objects or arrays, provide `.toString` method or convert manually.
194 |
195 | **NOTE**. Patches `history.push` and `history.replace` to enable `pushstate` and `replacestate` events.
196 |
197 | ```js
198 | function MyComponent () {
199 | let [id, setId] = useSearchParam('id')
200 | }
201 | ```
202 |
203 |
204 |
205 |
227 |
228 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
285 |
286 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
334 |
335 |
336 |
337 | useCountdown
338 |
339 | #### `[n, reset] = useCountdown(startValue, interval=1000 | schedule?)`
340 |
341 | Countdown value from `startValue` down to `0` with indicated `interval` in ms. Alternatively, a scheduler function can be passed as `schedule` argument, that can be eg. [worker-timers](https://ghub.io/worker-timers)-based implementation.
342 |
343 | ```js
344 | import { useCountdown } from 'unihooks'
345 | import { setInterval, clearInterval } from 'worker-timers'
346 |
347 | const Demo = () => {
348 | const [count, reset] = useCountdown(30, fn => {
349 | let id = setInterval(fn, 1000)
350 | return () => clearInterval(id)
351 | });
352 |
353 | return `Remains: ${count}s`
354 | };
355 | ```
356 |
357 |
358 |
359 |
360 | useValidate
361 |
362 | #### `[error, validate] = useValidate(validator: Function | Array, init? )`
363 |
364 | Provides validation functionality.
365 |
366 | * `validator` is a function or an array of functions `value => error | true ?`.
367 | * `init` is optional initial value to validate.
368 |
369 | ```js
370 | function MyComponent () {
371 | let [usernameError, validateUsername] = useValidate([
372 | value => !value ? 'Username is required' : true,
373 | value => value.length < 2 ? 'Username must be at least 2 chars long' : true
374 | ])
375 |
376 | return <>
377 | validateUsername(e.target.value) && handleInputChange(e) } {...inputProps}/>
378 | { usernameError }
379 | >
380 | }
381 | ```
382 |
383 |
384 |
385 |
386 | useFormField
387 |
388 | #### `[props, field] = useFormField( options )`
389 |
390 | Form field state controller. Handles input state and validation.
391 | Useful for organizing controlled inputs or forms, a nice minimal replacement to form hooks libraries.
392 |
393 | ```js
394 | let [props, field] = useFormField({
395 | name: 'password',
396 | type: 'password',
397 | validate: value => !!value
398 | })
399 |
400 | // to set new input value
401 | useEffect(() => field.set(newValue))
402 |
403 | return
404 | ```
405 |
406 | #### `options`
407 |
408 | * `value` - initial input value.
409 | * `persist = false` - persist input state between sessions.
410 | * `validate` - custom validator for input, modifies `field.error`. See `useValidate`.
411 | * `required` - if value must not be empty.
412 | * `...props` - the rest of props is passed to `props`
413 |
414 | #### `field`
415 |
416 | * `value` - current input value.
417 | * `error` - current validation error. Revalidates on blur, `null` on focus.
418 | * `valid: bool` - is valid value, revalidates on blur.
419 | * `focus: bool` - if input is focused.
420 | * `touched: bool` - if input was focused.
421 | * `set(value)` - set input value.
422 | * `reset()` - reset form state to initial.
423 | * `validate(value)` - force-validate input.
424 |
425 |
426 |
427 |
428 | useInput
429 |
430 | #### `[value, setValue] = useInput( element | ref )`
431 |
432 | Uncontrolled input element hook. Updates if input value changes.
433 | Setting `null` / `undefined` removes attribute from element.
434 | Useful for organizing simple input controllers, for advanced cases see [useFormField](#useFormField).
435 |
436 | ```js
437 | function MyButton() {
438 | let ref = useRef()
439 | let [value, setValue] = useInput(ref)
440 |
441 | useEffect(() => {
442 | // side-effect when value changes
443 | }, [value])
444 |
445 | return
446 | }
447 | ```
448 |
449 |
450 |
451 |
452 | useObservable
453 |
454 | #### `[state, setState] = useObservable(observable)`
455 |
456 | Observable as hook. Plug in any [spect/v](https://ghub.io/spect), [observable](https://ghub.io/observable), [mutant](https://ghub.io/mutant), [observ](https://ghub.io/observ) be free.
457 |
458 | ```js
459 | import { v } from 'spect/v'
460 |
461 | const vCount = v(0)
462 |
463 | function MyComponent () {
464 | let [count, setCount] = useObservable(vCount)
465 |
466 | useEffect(() => {
467 | let id = setInterval(() => setCount(count++), 1000)
468 | return () => clearInterval(id)
469 | }, [])
470 |
471 | return <>Count: { count }>
472 | }
473 | ```
474 |
475 |
476 |
524 |
525 |
526 |
527 |
528 |
529 |
530 |
531 |
532 |
533 |
534 |
535 |
536 |
537 |
538 |
539 |
540 |
541 |
542 |
543 |
544 |
545 |
546 |
547 |
548 |
549 |
550 |
551 |
552 |
553 |
554 |
555 |
556 |
557 |
558 |
559 |
560 |
561 |
562 |
563 |
564 |
565 |
566 |
567 |
568 |
569 |
570 |
571 |
572 |
573 |
574 |
575 |
576 |
577 |
578 |
579 |
580 |
581 |
582 |
583 |
584 |
590 |
591 |
592 |
593 |
594 |
595 |
596 |
597 |
598 |
599 |
600 |
601 |
602 |
603 |
604 |
605 | standard
606 |
607 |
608 | For convenience, unihooks export current framework hooks. To switch hooks, use `setHooks` - the default export.
609 |
610 | ```js
611 | import setHooks, { useState, useEffect } from 'unihooks'
612 | import * as hooks from 'preact/hooks'
613 |
614 | setHooks(hooks)
615 |
616 | function Timer() {
617 | let [count, setCount] = useState(0)
618 | useEffect(() => {
619 | let id = setInterval(() => setCount(c => ++c))
620 | return () => clearInterval(id)
621 | }, [])
622 | }
623 | ```
624 |
625 |
626 |
627 |
628 | utility
629 |
630 |
631 | Utility hooks, useful for high-order-hooks.
632 |
633 | #### `update = useUpdate()`
634 |
635 | Force-update component, regardless of internal state.
636 |
637 | #### `prev = usePrevious(value)`
638 |
639 | Returns the previous state as described in the [React hooks FAQ](https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state).
640 |
641 |
647 |
648 |
649 |
650 | ## See also
651 |
652 | * [any-hooks](https://ghub.io/any-hooks) - cross-framework standard hooks provider.
653 | * [enhook](https://ghub.io/enhook) - run hooks in regular functions.
654 |
655 | ## Alternatives
656 |
657 | * [valtio](https://github.com/pmndrs/valtio)
658 |
659 | ## License
660 |
661 | MIT
662 |
663 | HK
664 |
--------------------------------------------------------------------------------