extra-react-hooks 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ import { AbortController } from 'extra-abort';
4
4
  const abort = Symbol();
5
5
  export function useCallbackAsync(callback, deps) {
6
6
  const controller = useMemo(() => new AbortController(), deps);
7
- useEffect(() => () => controller.abort(abort), deps);
7
+ useEffect(() => () => controller.abort(abort), [controller]);
8
8
  return useCallback((...args) => {
9
9
  go(async () => {
10
10
  try {
@@ -15,6 +15,6 @@ export function useCallbackAsync(callback, deps) {
15
15
  throw err;
16
16
  }
17
17
  });
18
- }, deps);
18
+ }, [callback, controller]);
19
19
  }
20
20
  //# sourceMappingURL=use-callback-async.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-callback-async.js","sourceRoot":"","sources":["../src/use-callback-async.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,OAAO,EAAkB,MAAM,OAAO,CAAA;AACvE,OAAO,EAAE,EAAE,EAAE,MAAM,qBAAqB,CAAA;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAE7C,MAAM,KAAK,GAAG,MAAM,EAAE,CAAA;AAEtB,MAAM,UAAU,gBAAgB,CAC9B,QAA0E,EAC1E,IAAoB;IAEpB,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,eAAe,EAAE,EAAE,IAAI,CAAC,CAAA;IAC7D,SAAS,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,CAAA;IAEpD,OAAO,WAAW,CAAC,CAAC,GAAG,IAAU,EAAE,EAAE;QACnC,EAAE,CAAC,KAAK,IAAI,EAAE;YACZ,IAAI;gBACF,MAAM,QAAQ,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC,CAAA;aAC3C;YAAC,OAAO,GAAG,EAAE;gBACZ,IAAI,GAAG,KAAK,KAAK;oBAAE,MAAM,GAAG,CAAA;aAC7B;QACH,CAAC,CAAC,CAAA;IACJ,CAAC,EAAE,IAAI,CAAC,CAAA;AACV,CAAC"}
1
+ {"version":3,"file":"use-callback-async.js","sourceRoot":"","sources":["../src/use-callback-async.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,OAAO,EAAkB,MAAM,OAAO,CAAA;AACvE,OAAO,EAAE,EAAE,EAAE,MAAM,qBAAqB,CAAA;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAE7C,MAAM,KAAK,GAAG,MAAM,EAAE,CAAA;AAEtB,MAAM,UAAU,gBAAgB,CAC9B,QAA0E,EAC1E,IAAoB;IAEpB,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,eAAe,EAAE,EAAE,IAAI,CAAC,CAAA;IAC7D,SAAS,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAA;IAE5D,OAAO,WAAW,CAAC,CAAC,GAAG,IAAU,EAAE,EAAE;QACnC,EAAE,CAAC,KAAK,IAAI,EAAE;YACZ,IAAI;gBACF,MAAM,QAAQ,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC,CAAA;aAC3C;YAAC,OAAO,GAAG,EAAE;gBACZ,IAAI,GAAG,KAAK,KAAK;oBAAE,MAAM,GAAG,CAAA;aAC7B;QACH,CAAC,CAAC,CAAA;IACJ,CAAC,EAAE,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAA;AAC5B,CAAC"}
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "extra-react-hooks",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "files": [
7
- "lib"
7
+ "lib",
8
+ "src"
8
9
  ],
9
10
  "type": "module",
10
11
  "main": "lib/index.js",
@@ -33,19 +34,19 @@
33
34
  }
34
35
  },
35
36
  "devDependencies": {
36
- "@blackglory/wait-for": "^0.7.3",
37
- "@commitlint/cli": "^17.6.1",
38
- "@commitlint/config-conventional": "^17.6.1",
37
+ "@blackglory/wait-for": "^0.7.4",
38
+ "@commitlint/cli": "^17.6.5",
39
+ "@commitlint/config-conventional": "^17.6.5",
39
40
  "@testing-library/react": "^14.0.0",
40
- "@types/jasmine": "^4.3.1",
41
- "@types/react": "^18.0.35",
42
- "@typescript-eslint/eslint-plugin": "^5.58.0",
43
- "@typescript-eslint/parser": "^5.58.0",
44
- "eslint": "^8.38.0",
41
+ "@types/jasmine": "^4.3.2",
42
+ "@types/react": "^18.2.11",
43
+ "@typescript-eslint/eslint-plugin": "^5.59.9",
44
+ "@typescript-eslint/parser": "^5.59.9",
45
+ "eslint": "^8.42.0",
45
46
  "eslint-plugin-react-hooks": "^4.6.0",
46
47
  "husky": "4",
47
- "karma": "^6.4.1",
48
- "karma-chrome-launcher": "^3.1.1",
48
+ "karma": "^6.4.2",
49
+ "karma-chrome-launcher": "^3.2.0",
49
50
  "karma-jasmine": "^5.1.0",
50
51
  "karma-webpack": "^5.0.0",
51
52
  "npm-run-all": "^4.1.5",
@@ -54,19 +55,19 @@
54
55
  "react-test-renderer": "^18.2.0",
55
56
  "rimraf": "^4.3.0",
56
57
  "standard-version": "^9.3.2",
57
- "ts-loader": "^9.4.2",
58
+ "ts-loader": "^9.4.3",
58
59
  "ts-patch": "^2.1.0",
59
60
  "tsconfig-paths-webpack-plugin": "^4.0.1",
60
- "tslib": "^2.4.1",
61
+ "tslib": "^2.5.3",
61
62
  "typescript": "4.9.5",
62
63
  "typescript-transform-paths": "^3.4.6",
63
- "webpack": "^5.79.0"
64
+ "webpack": "^5.86.0"
64
65
  },
65
66
  "dependencies": {
66
- "@blackglory/prelude": "^0.3.2",
67
- "extra-abort": "^0.3.6",
68
- "extra-fsm": "^0.2.0",
69
- "rxjs": "^7.8.0"
67
+ "@blackglory/prelude": "^0.3.3",
68
+ "extra-abort": "^0.3.7",
69
+ "extra-fsm": "^0.2.1",
70
+ "rxjs": "^7.8.1"
70
71
  },
71
72
  "peerDependencies": {
72
73
  "react": "^16.8 || ^17 || ^18"
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ export * from './use-increment.js'
2
+ export * from './use-is-mounted.js'
3
+ export * from './use-is-first-render.js'
4
+ export * from './use-render-counter.js'
5
+ export * from './use-mount.js'
6
+ export * from './use-mount-async.js'
7
+ export * from './use-unmount.js'
8
+ export * from './use-effect-async.js'
9
+ export * from './use-callback-async.js'
10
+ export * from './use-update-effect.js'
11
+ export * from './use-single-selection.js'
12
+ export * from './use-multiple-selection.js'
13
+ export * from './use-state-cycle.js'
14
+ export * from './use-toggle.js'
15
+ export * from './use-resize-observer.js'
16
+ export * from './use-step.js'
17
+ export * from './use-force-update.js'
18
+ export * from './use-finite-state-machine.js'
19
+ export * from './use-iife.js'
package/src/types.ts ADDED
@@ -0,0 +1,3 @@
1
+ export interface IOptionState {
2
+ selected: boolean
3
+ }
@@ -0,0 +1,23 @@
1
+ import { useEffect, useCallback, useMemo, DependencyList } from 'react'
2
+ import { go } from '@blackglory/prelude'
3
+ import { AbortController } from 'extra-abort'
4
+
5
+ const abort = Symbol()
6
+
7
+ export function useCallbackAsync<Args extends unknown[]>(
8
+ callback: (...args: [...args: Args, signal: AbortSignal]) => Promise<void>
9
+ , deps: DependencyList
10
+ ): (...args: Args) => void {
11
+ const controller = useMemo(() => new AbortController(), deps)
12
+ useEffect(() => () => controller.abort(abort), [controller])
13
+
14
+ return useCallback((...args: Args) => {
15
+ go(async () => {
16
+ try {
17
+ await callback(...args, controller.signal)
18
+ } catch (err) {
19
+ if (err !== abort) throw err
20
+ }
21
+ })
22
+ }, [callback, controller])
23
+ }
@@ -0,0 +1,24 @@
1
+ import { useEffect, DependencyList } from 'react'
2
+ import { go } from '@blackglory/prelude'
3
+ import { AbortController } from 'extra-abort'
4
+
5
+ const abort = Symbol()
6
+
7
+ export function useEffectAsync(
8
+ effect: (signal: AbortSignal) => Promise<void>
9
+ , deps?: DependencyList
10
+ ): void {
11
+ useEffect(() => {
12
+ const controller = new AbortController()
13
+
14
+ go(async () => {
15
+ try {
16
+ await effect(controller.signal)
17
+ } catch (err) {
18
+ if (err !== abort) throw err
19
+ }
20
+ })
21
+
22
+ return () => controller.abort()
23
+ }, deps)
24
+ }
@@ -0,0 +1,28 @@
1
+ import { useMemo, useEffect } from 'react'
2
+ import { ObservableFiniteStateMachine, IFiniteStateMachineSchema } from 'extra-fsm'
3
+ import { useForceUpdate } from './use-force-update.js'
4
+
5
+ export type {
6
+ ObservableFiniteStateMachine
7
+ , IFiniteStateMachineSchema
8
+ } from 'extra-fsm'
9
+
10
+ export function useFiniteStateMachine<State extends string, Event extends string>(
11
+ schema: IFiniteStateMachineSchema<State, Event>
12
+ , initialState: State
13
+ ): ObservableFiniteStateMachine<State, Event> {
14
+ const forceUpdate = useForceUpdate()
15
+ const fsm = useMemo(
16
+ () => new ObservableFiniteStateMachine(schema, initialState)
17
+ , [schema, initialState]
18
+ )
19
+
20
+ useEffect(() => {
21
+ const observable = fsm.observeStateChanges()
22
+ const subscription = observable.subscribe(() => forceUpdate())
23
+
24
+ return () => subscription.unsubscribe()
25
+ }, [fsm])
26
+
27
+ return fsm
28
+ }
@@ -0,0 +1,11 @@
1
+ import { useState, useCallback } from 'react'
2
+
3
+ export function useForceUpdate(): () => void {
4
+ const [, setState] = useState({})
5
+
6
+ return useCallback(forceUpdate, [])
7
+
8
+ function forceUpdate(): void {
9
+ setState({})
10
+ }
11
+ }
@@ -0,0 +1,21 @@
1
+ import { useRef } from 'react'
2
+ import { useIsFirstRender } from '@src/use-is-first-render.js'
3
+
4
+ export function useIIFE(iife: () => void, deps: React.DependencyList): void {
5
+ const oldDeps = useRef<React.DependencyList>([])
6
+ const isFirstRender = useIsFirstRender()
7
+
8
+ if (isFirstRender()) {
9
+ iife()
10
+ } else {
11
+ if (deps.length === oldDeps.current.length) {
12
+ if (deps.some((x, i) => x !== oldDeps.current[i])) {
13
+ iife()
14
+ }
15
+ } else {
16
+ iife()
17
+ }
18
+ }
19
+
20
+ oldDeps.current = Array.from(deps)
21
+ }
@@ -0,0 +1,16 @@
1
+ import { useState, useCallback } from 'react'
2
+
3
+ export function useIncrement(
4
+ initialValue: number
5
+ ): [value: number, increment: (step?: number) => void, reset: () => void] {
6
+ const [value, setValue] = useState(initialValue)
7
+ return [value, useCallback(increment, []), useCallback(reset, [])]
8
+
9
+ function increment(step: number = 1): void {
10
+ setValue(value => value + step)
11
+ }
12
+
13
+ function reset(): void {
14
+ setValue(initialValue)
15
+ }
16
+ }
@@ -0,0 +1,27 @@
1
+ import { useRef, useCallback } from 'react'
2
+
3
+ enum State {
4
+ InitialValue
5
+ , First
6
+ , NotFirst
7
+ }
8
+
9
+ export function useIsFirstRender(): () => boolean {
10
+ const state = useRef(State.InitialValue)
11
+
12
+ switch (state.current) {
13
+ case State.InitialValue:
14
+ state.current = State.First
15
+ break
16
+ case State.First:
17
+ state.current = State.NotFirst
18
+ break
19
+ case State.NotFirst: break
20
+ }
21
+
22
+ return useCallback(isFirstRender, [])
23
+
24
+ function isFirstRender(): boolean {
25
+ return state.current === State.First
26
+ }
27
+ }
@@ -0,0 +1,19 @@
1
+ import { useRef, useCallback } from 'react'
2
+ import { useMount } from './use-mount.js'
3
+
4
+ export function useIsMounted(): () => boolean {
5
+ const mounted = useRef(false)
6
+
7
+ useMount(() => {
8
+ mounted.current = true
9
+ return () => {
10
+ mounted.current = false
11
+ }
12
+ })
13
+
14
+ return useCallback(isMounted, [])
15
+
16
+ function isMounted(): boolean {
17
+ return mounted.current
18
+ }
19
+ }
@@ -0,0 +1,5 @@
1
+ import { useEffectAsync } from './use-effect-async.js'
2
+
3
+ export function useMountAsync(effect: (signal: AbortSignal) => Promise<void>): void {
4
+ useEffectAsync(effect, [])
5
+ }
@@ -0,0 +1,5 @@
1
+ import { useEffect, EffectCallback } from 'react'
2
+
3
+ export function useMount(effect: EffectCallback): void {
4
+ useEffect(effect, [])
5
+ }
@@ -0,0 +1,69 @@
1
+ import { useState, useMemo, useCallback } from 'react'
2
+ import { assert, toArray, NonEmptyArray } from '@blackglory/prelude'
3
+ import { IOptionState } from '@src/types.js'
4
+ import { useIIFE } from '@src/use-iife.js'
5
+
6
+ export function useMultipleSelection<T>(
7
+ options: NonEmptyArray<T>
8
+ , defaultSelectedIndexes: number[] = []
9
+ ): {
10
+ selectedIndexes: number[]
11
+ optionStates: IOptionState[]
12
+ toggle: (index: number) => void
13
+ select: (index: number) => void
14
+ unselect: (index: number) => void
15
+ } {
16
+ useIIFE(() => {
17
+ for (const index of defaultSelectedIndexes) {
18
+ assert(
19
+ index >= 0 && index < options.length
20
+ , 'The index of parameter defaultSelectedIndexes must be in the range of 0 to values.length'
21
+ )
22
+ }
23
+ }, [defaultSelectedIndexes])
24
+
25
+ const [selectedIndexes, setSelectedIndexes] = useState(new Set(defaultSelectedIndexes))
26
+
27
+ return {
28
+ selectedIndexes: useMemo(() => toArray(selectedIndexes), [selectedIndexes])
29
+ , optionStates: useMemo(createOptionStates, [options, selectedIndexes])
30
+ , toggle: useCallback(toggle, [selectedIndexes])
31
+ , select: useCallback(select, [])
32
+ , unselect: useCallback(unselect, [])
33
+ }
34
+
35
+ function createOptionStates(): IOptionState[] {
36
+ return options.map((_, index) => {
37
+ const selected = isSelected(index)
38
+ return { selected }
39
+ })
40
+ }
41
+
42
+ function isSelected(index: number): boolean {
43
+ return selectedIndexes.has(index)
44
+ }
45
+
46
+ function toggle(index: number): void {
47
+ if (isSelected(index)) {
48
+ unselect(index)
49
+ } else {
50
+ select(index)
51
+ }
52
+ }
53
+
54
+ function select(index: number): void {
55
+ setSelectedIndexes(selectedIndexes => {
56
+ const result = new Set(selectedIndexes)
57
+ result.add(index)
58
+ return result
59
+ })
60
+ }
61
+
62
+ function unselect(index: number): void {
63
+ setSelectedIndexes(selectedIndexes => {
64
+ const result = new Set(selectedIndexes)
65
+ result.delete(index)
66
+ return result
67
+ })
68
+ }
69
+ }
@@ -0,0 +1,9 @@
1
+ import { useRef } from 'react'
2
+
3
+ export function useRenderCounter(): number {
4
+ const counter = useRef(0)
5
+
6
+ counter.current++
7
+
8
+ return counter.current
9
+ }
@@ -0,0 +1,20 @@
1
+ import { useEffect, RefObject, MutableRefObject } from 'react'
2
+ import { isntNull } from '@blackglory/prelude'
3
+
4
+ export function useResizeObserver(
5
+ callback: ResizeObserverCallback
6
+ , refs: Array<RefObject<HTMLElement> | MutableRefObject<HTMLElement>>
7
+ ): void {
8
+ const deps = refs.map(x => x.current)
9
+
10
+ useEffect(() => {
11
+ const observer = new ResizeObserver(callback)
12
+
13
+ refs
14
+ .map(x => x.current)
15
+ .filter(isntNull)
16
+ .forEach(x => observer.observe(x))
17
+
18
+ return () => observer.disconnect()
19
+ }, deps)
20
+ }
@@ -0,0 +1,38 @@
1
+ import { useState, useMemo, useCallback } from 'react'
2
+ import { assert, NonEmptyArray } from '@blackglory/prelude'
3
+ import { IOptionState } from '@src/types.js'
4
+ import { useIIFE } from '@src/use-iife.js'
5
+
6
+ export function useSingleSelection<T>(
7
+ options: NonEmptyArray<T>
8
+ , defaultSelectedIndex: number
9
+ ): {
10
+ selectedIndex: number
11
+ optionStates: IOptionState[]
12
+ select: (index: number) => void
13
+ } {
14
+ useIIFE(() => {
15
+ assert(
16
+ defaultSelectedIndex >= 0 && defaultSelectedIndex < options.length
17
+ , 'The parameter defaultSelectedIndex must be in the range of 0 to values.length'
18
+ )
19
+ }, [defaultSelectedIndex])
20
+
21
+ const [selectedIndex, setSelectedIndex] = useState(defaultSelectedIndex)
22
+
23
+ return {
24
+ selectedIndex
25
+ , optionStates: useMemo(createOptionStates, [options, selectedIndex])
26
+ , select: useCallback(select, [])
27
+ }
28
+
29
+ function createOptionStates(): IOptionState[] {
30
+ return options.map((_, index) => ({
31
+ selected: index === selectedIndex
32
+ }))
33
+ }
34
+
35
+ function select(index: number): void {
36
+ setSelectedIndex(index)
37
+ }
38
+ }
@@ -0,0 +1,21 @@
1
+ import { useState, useCallback } from 'react'
2
+ import { NonEmptyArray } from '@blackglory/prelude'
3
+
4
+ export function useStateCycle<T>(
5
+ orderedStates: NonEmptyArray<T>
6
+ , initialStateIndex: number = 0
7
+ ): [state: T, next: () => void] {
8
+ const [index, setIndex] = useState(initialStateIndex)
9
+
10
+ return [orderedStates[index], useCallback(next, [orderedStates])]
11
+
12
+ function next(): void {
13
+ setIndex(index => {
14
+ if (index + 1 < orderedStates.length) {
15
+ return index + 1
16
+ } else {
17
+ return 0
18
+ }
19
+ })
20
+ }
21
+ }
@@ -0,0 +1,31 @@
1
+ import { useState, useCallback } from 'react'
2
+ import { NonEmptyArray } from '@blackglory/prelude'
3
+
4
+ export function useStep<T>(
5
+ steps: NonEmptyArray<T>
6
+ , initialStepIndex: number = 0
7
+ ): [currentStep: T, next: () => void, previous: () => void] {
8
+ const [index, setIndex] = useState(initialStepIndex)
9
+
10
+ return [steps[index], useCallback(next, [steps]), useCallback(previous, [])]
11
+
12
+ function next(): void {
13
+ setIndex(index => {
14
+ if (index === steps.length - 1) {
15
+ return index
16
+ } else {
17
+ return index + 1
18
+ }
19
+ })
20
+ }
21
+
22
+ function previous(): void {
23
+ setIndex(index => {
24
+ if (index === 0) {
25
+ return index
26
+ } else {
27
+ return index - 1
28
+ }
29
+ })
30
+ }
31
+ }
@@ -0,0 +1,12 @@
1
+ import { useState, useCallback } from 'react'
2
+
3
+ export function useToggle(
4
+ initialState: boolean = false
5
+ ): [on: boolean, toggle: () => void ] {
6
+ const [state, setState] = useState(initialState)
7
+ return [state, useCallback(toggle, [])]
8
+
9
+ function toggle(): void {
10
+ setState(state => !state)
11
+ }
12
+ }
@@ -0,0 +1,5 @@
1
+ import { useEffect } from 'react'
2
+
3
+ export function useUnmount(effect: () => void): void {
4
+ useEffect(() => effect, [])
5
+ }
@@ -0,0 +1,15 @@
1
+ import { useEffect, EffectCallback } from 'react'
2
+ import { useIsFirstRender } from './use-is-first-render.js'
3
+
4
+ export function useUpdateEffect(
5
+ effect: EffectCallback
6
+ , deps?: React.DependencyList
7
+ ): void {
8
+ const isFirstRender = useIsFirstRender()
9
+
10
+ useEffect(() => {
11
+ if (!isFirstRender()) {
12
+ return effect()
13
+ }
14
+ }, deps)
15
+ }