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.
- package/lib/use-callback-async.js +2 -2
- package/lib/use-callback-async.js.map +1 -1
- package/package.json +20 -19
- package/src/index.ts +19 -0
- package/src/types.ts +3 -0
- package/src/use-callback-async.ts +23 -0
- package/src/use-effect-async.ts +24 -0
- package/src/use-finite-state-machine.ts +28 -0
- package/src/use-force-update.ts +11 -0
- package/src/use-iife.ts +21 -0
- package/src/use-increment.ts +16 -0
- package/src/use-is-first-render.ts +27 -0
- package/src/use-is-mounted.ts +19 -0
- package/src/use-mount-async.ts +5 -0
- package/src/use-mount.ts +5 -0
- package/src/use-multiple-selection.ts +69 -0
- package/src/use-render-counter.ts +9 -0
- package/src/use-resize-observer.ts +20 -0
- package/src/use-single-selection.ts +38 -0
- package/src/use-state-cycle.ts +21 -0
- package/src/use-step.ts +31 -0
- package/src/use-toggle.ts +12 -0
- package/src/use-unmount.ts +5 -0
- package/src/use-update-effect.ts +15 -0
|
@@ -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),
|
|
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
|
-
},
|
|
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,
|
|
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.
|
|
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.
|
|
37
|
-
"@commitlint/cli": "^17.6.
|
|
38
|
-
"@commitlint/config-conventional": "^17.6.
|
|
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.
|
|
41
|
-
"@types/react": "^18.
|
|
42
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
43
|
-
"@typescript-eslint/parser": "^5.
|
|
44
|
-
"eslint": "^8.
|
|
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.
|
|
48
|
-
"karma-chrome-launcher": "^3.
|
|
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.
|
|
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.
|
|
61
|
+
"tslib": "^2.5.3",
|
|
61
62
|
"typescript": "4.9.5",
|
|
62
63
|
"typescript-transform-paths": "^3.4.6",
|
|
63
|
-
"webpack": "^5.
|
|
64
|
+
"webpack": "^5.86.0"
|
|
64
65
|
},
|
|
65
66
|
"dependencies": {
|
|
66
|
-
"@blackglory/prelude": "^0.3.
|
|
67
|
-
"extra-abort": "^0.3.
|
|
68
|
-
"extra-fsm": "^0.2.
|
|
69
|
-
"rxjs": "^7.8.
|
|
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,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
|
+
}
|
package/src/use-iife.ts
ADDED
|
@@ -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
|
+
}
|
package/src/use-mount.ts
ADDED
|
@@ -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,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
|
+
}
|
package/src/use-step.ts
ADDED
|
@@ -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,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
|
+
}
|