-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstuple-memo.ts
More file actions
136 lines (122 loc) · 4.29 KB
/
Copy pathstuple-memo.ts
File metadata and controls
136 lines (122 loc) · 4.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import type { SetStateAction } from 'react'
import { useCallback, useMemo, useRef } from 'react'
import { useTriggerRerender } from './react-simple-util'
import {
applySubSlice,
asStuple,
type ObjectNotArray,
type SetState,
type Stuple,
type UsedState,
} from './stuple-core'
export type { SetState, Stuple, UsedState } from './stuple-core'
/**
* Hook version of {@link subSetState}. The returned `set` keeps the same reference across renders
* (until `outerSetState` or `key` changes), so it is safe in `useCallback` / `useEffect` dependencies.
*
* Prefer this over {@link subSetState} when the setter is created during component render.
*/
export function useSubSetState<
T extends any[],
K extends number & keyof T,
U extends T[K],
>(outerSetState: SetState<T>, key: K, initialValue?: U): SetState<U>
export function useSubSetState<
T extends ObjectNotArray,
K extends keyof T,
U extends T[K],
>(outerSetState: SetState<T>, key: K, initialValue?: U): SetState<U>
export function useSubSetState<
T extends ObjectNotArray | any[],
K extends T extends any[] ? number & keyof T : keyof T,
U extends T[K],
>(outerSetState: SetState<T>, key: K, initialValue?: U): SetState<U> {
const initialRef = useRef(initialValue)
initialRef.current = initialValue
return useCallback(
(nextValue) => {
outerSetState((prev) =>
applySubSlice(
prev,
key,
nextValue as SetStateAction<T[K]>,
initialRef.current,
),
)
},
[outerSetState, key],
) as SetState<U>
}
/**
* React hook version of {@link subStuple}. Prefer this over {@link subStuple} inside components.
*
* {@link subStuple} builds a new `set` function on every call, which breaks `useCallback` / `useEffect`
* dependency lists and can cause infinite fetch loops. `useSubStuple` uses {@link useSubSetState} so `set`
* stays referentially stable across renders (while `val` still updates when the parent slice changes).
*
* @example
* ```tsx
* const panels = useStuple<Record<string, PanelState>>(() => ({}))
* const panel = useSubStuple(panels, activeRepoId, defaultPanelState)
* // safe: useCallback(() => fetch(panel.val), [panel.set, activeRepoId])
* ```
*/
export function useSubStuple<
T extends any[],
K extends number & keyof T,
U extends T[K],
>(outerStuple: Stuple<T>, key: K, initialValue?: U): Stuple<T[K]>
export function useSubStuple<
T extends ObjectNotArray,
K extends keyof T,
U extends T[K],
>(outerStuple: Stuple<T>, key: K, initialValue?: U): Stuple<T[K]>
export function useSubStuple<
T extends ObjectNotArray | any[],
K extends T extends any[] ? number & keyof T : keyof T,
U extends T[K],
>(outerStuple: Stuple<T>, key: K, initialValue?: U): Stuple<T[K]> {
const parentValue = outerStuple.val
const initialRef = useRef(initialValue)
initialRef.current = initialValue
const val = (
parentValue[key] === undefined ?
initialRef.current
: parentValue[key]) as T[K]
// @ts-expect-error — overloaded slice types (array vs object parent)
const set: SetState<T[K]> = useSubSetState(outerStuple.set, key, initialValue)
return useMemo(() => ({ val, set }), [val, set])
}
/**
* Wipes and resets the local state value when any dependency changes.
*/
export function useStateWithDeps<T>(init: () => T, deps: any[]): UsedState<T> {
const valueRef = useRef<{ deps: any[]; value: T }>(undefined)
const triggerRerender = useTriggerRerender()
const set: SetState<T> = useCallback(
(nextState: SetStateAction<T>) => {
const newValue =
typeof nextState === 'function' ?
(nextState as (oldState: T) => T)(valueRef.current!.value)
: nextState
const different = valueRef.current!.value !== newValue
valueRef.current!.value = newValue
if (different) triggerRerender()
},
[triggerRerender],
)
if (!valueRef.current) {
valueRef.current = { deps, value: init() }
} else {
if (!valueRef.current.deps.every((v, i) => v === deps[i])) {
valueRef.current = { deps, value: init() }
}
}
return [valueRef.current.value, set]
}
/**
* Same as {@link useStateWithDeps} but returns the {@link Stuple} format instead of {@link UsedState}.
*/
export function useStupleWithDeps<T>(init: () => T, deps: any[]): Stuple<T> {
return asStuple(useStateWithDeps(init, deps))
}