Skip to content

Commit ffc9c6d

Browse files
committed
Add use-array
1 parent 3bd8ed1 commit ffc9c6d

File tree

3 files changed

+196
-7
lines changed

3 files changed

+196
-7
lines changed

hooks/use-array.ts

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { type SetStateAction, useState, useEffect } from 'react'
2+
3+
/**
4+
* This is an array that causes rerenders on updates
5+
* @note Effects and memos that use this array should also listen for its signal: `+INSTANCE`
6+
*/
7+
export class StatefulArray<T> extends Array<T> {
8+
/** The dispatch function for the signal */
9+
private readonly _dispatchSignal?: React.Dispatch<SetStateAction<number>>
10+
/** The update signal */
11+
private _signal: number
12+
/** THe dispatch function for redefining the set */
13+
private _dispatchRedefine?: React.Dispatch<SetStateAction<StatefulArray<T>>>
14+
15+
/**
16+
* Construct a StatefulSet
17+
* @param initial The initial value (parameter for a vanilla set)
18+
* @param dispatchSignal The dispatch function for the signal
19+
*/
20+
constructor (initial?: Iterable<T>, dispatchSignal?: StatefulArray<T>['_dispatchSignal']) {
21+
if (initial) super(...initial)
22+
else super()
23+
this._signal = 0
24+
this._dispatchSignal = dispatchSignal
25+
}
26+
27+
/**
28+
* Set the redefine dispatch
29+
* @private
30+
* @param callback The function
31+
*/
32+
_setRedefine (callback: StatefulArray<T>['_dispatchRedefine']): void {
33+
this._dispatchRedefine = callback
34+
}
35+
36+
/**
37+
* Force a signal update
38+
*/
39+
forceUpdate (): void {
40+
this._dispatchSignal?.(++this._signal)
41+
}
42+
43+
/**
44+
* Set the instance to an entirely new instance
45+
* @param value The new instance
46+
* @returns The new instance
47+
* @throws {Error} If there's no redefinition callback defined
48+
*/
49+
reset (value: Iterable<T>): Iterable<T> {
50+
if (!this._dispatchRedefine) throw new Error('Cannot redefine Array. No redefine callback set.')
51+
const instance = new StatefulArray(value, this._dispatchSignal)
52+
if (this._signal === 0) instance._signal = 1 // Force update
53+
54+
this._dispatchRedefine(instance)
55+
instance._dispatchSignal?.(instance._signal)
56+
57+
return instance
58+
}
59+
60+
/**
61+
* @override
62+
*/
63+
override copyWithin (target: number, start: number, end?: number): this {
64+
let different = false
65+
for (let offset = 0; offset < (end ?? this.length - 1); ++offset) {
66+
if (this[target + offset] !== this[start + offset]) {
67+
different = true
68+
break
69+
}
70+
}
71+
72+
if (different) this._dispatchSignal?.(++this._signal)
73+
return super.copyWithin(target, start, end)
74+
}
75+
76+
/**
77+
* @override
78+
*/
79+
override fill (value: T, start?: number, end?: number): this {
80+
let different = false
81+
for (let i = start ?? 0; i < (end ?? this.length - 1); ++i) {
82+
if (this[i] !== value) {
83+
different = true
84+
break
85+
}
86+
}
87+
88+
if (different) this._dispatchSignal?.(++this._signal)
89+
return super.fill(value, start, end)
90+
}
91+
92+
/**
93+
* @override
94+
*/
95+
override pop (): T | undefined {
96+
if (this.length) this._dispatchSignal?.(++this._signal)
97+
return super.pop()
98+
}
99+
100+
/**
101+
* @override
102+
*/
103+
override push (...items: T[]): number {
104+
if (items.length) this._dispatchSignal?.(++this._signal)
105+
return super.push(...items)
106+
}
107+
108+
/**
109+
* @override
110+
*/
111+
override reverse (): T[] {
112+
let palindrome = true
113+
for (let i = 0; i < Math.floor(this.length / 2); ++i) {
114+
if (this[i] !== this[this.length - i - 1]) {
115+
palindrome = false
116+
break
117+
}
118+
}
119+
120+
if (!palindrome) this._dispatchSignal?.(++this._signal)
121+
return super.reverse()
122+
}
123+
124+
/**
125+
* @override
126+
*/
127+
override shift (): T | undefined {
128+
if (this.length) this._dispatchSignal?.(++this._signal)
129+
return super.shift()
130+
}
131+
132+
/**
133+
* @override
134+
*/
135+
override sort (compareFn?: ((a: T, b: T) => number)): this {
136+
// No way to efficiently compare this without copying; always signal
137+
this._dispatchSignal?.(++this._signal)
138+
return this.sort(compareFn)
139+
}
140+
141+
/**
142+
* @override
143+
* @overload
144+
*/
145+
override splice (start: number, deleteCount?: number): T[]
146+
/**
147+
* @override
148+
*/
149+
override splice (start: number, deleteCount: number, ...rest: T[]): T[] {
150+
if (deleteCount || rest.length) this._dispatchSignal?.(++this._signal)
151+
return super.splice(start, deleteCount, ...rest)
152+
}
153+
154+
/**
155+
* @override
156+
*/
157+
override unshift (...items: T[]): number {
158+
if (items.length) this._dispatchSignal?.(++this._signal)
159+
return super.unshift(...items)
160+
}
161+
162+
/**
163+
* Returns the set's signal. Used for effects and memos that use this set
164+
* @returns A numeric signal
165+
*/
166+
override valueOf (): number {
167+
return this._signal
168+
}
169+
}
170+
171+
/**
172+
* Use a stately array
173+
* @note Any effects or memos that use this set should also listen for its signal (`+INSTANCE`)
174+
* @param initial The initial array value
175+
* @returns The stately array
176+
*/
177+
export function useArray<T> (initial?: Iterable<T>): StatefulArray<T> {
178+
const [, setSignal] = useState(0)
179+
const [array, setArray] = useState(new StatefulArray(initial, setSignal))
180+
181+
useEffect(() => array._setRedefine(setArray), [array])
182+
183+
return array
184+
}

hooks/use-object.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { useMemo, useState } from 'react'
1+
import { useCallback, useMemo, useState } from 'react'
22

33
/**
44
* Create an object state value that auto updates on mutation
5+
* @note Effects and memos that use this object should also listen for its signal: `+INSTANCE`
56
* @param initial The initial object
6-
* @returns [object, setObject]
7+
* @returns [object, setObject, forceUpdate]
78
*/
8-
export function useObject<T extends object> (initial: T): [object: T, setObject: React.Dispatch<React.SetStateAction<T>>] {
9+
export function useObject<T extends object> (initial: T): [object: T, setObject: React.Dispatch<React.SetStateAction<T>>, forceUpdate: () => void] {
910
const [signal, setSignal] = useState(0)
1011
const [object, setObject] = useState(initial)
1112

@@ -23,6 +24,10 @@ export function useObject<T extends object> (initial: T): [object: T, setObject:
2324
}
2425
}), [object])
2526

27+
const forceUpdate = useCallback(() => {
28+
setSignal((prior) => prior + 1)
29+
}, [])
30+
2631
proxy.valueOf = () => signal
27-
return [proxy, setObject]
32+
return [proxy, setObject, forceUpdate]
2833
}

hooks/use-set.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export class StatefulSet<T> extends Set<T> {
3333
* Force a signal update
3434
*/
3535
forceUpdate (): void {
36-
this._dispatchSignal?.(-1)
36+
this._dispatchSignal?.(-Math.random())
3737
}
3838

3939
/**
@@ -111,8 +111,8 @@ export class StatefulSet<T> extends Set<T> {
111111
* @param initial The initial set value
112112
* @returns The stately set
113113
*/
114-
export function useSet<T> (initial?: Set<T> | T[]): StatefulSet<T> {
115-
const [, setSignal] = useState(Array.isArray(initial) ? initial.length : initial?.size ?? 0)
114+
export function useSet<T> (initial?: Iterable<T>): StatefulSet<T> {
115+
const [, setSignal] = useState(Array.isArray(initial) ? initial.length : initial instanceof Set ? initial.size : 0)
116116
const [set, setSet] = useState(new StatefulSet(initial, setSignal))
117117

118118
useEffect(() => set._setRedefine(setSet), [set])

0 commit comments

Comments
 (0)