Skip to content

Commit 017987a

Browse files
committed
Add use-map
0 parents  commit 017987a

File tree

7 files changed

+253
-0
lines changed

7 files changed

+253
-0
lines changed

.gitignore

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# dependencies (bun install)
2+
node_modules
3+
4+
# output
5+
out
6+
dist
7+
*.tgz
8+
9+
# code coverage
10+
coverage
11+
*.lcov
12+
13+
# logs
14+
logs
15+
_.log
16+
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
17+
18+
# dotenv environment variable files
19+
.env
20+
.env.development.local
21+
.env.test.local
22+
.env.production.local
23+
.env.local
24+
25+
# caches
26+
.eslintcache
27+
.cache
28+
*.tsbuildinfo
29+
30+
# IntelliJ based IDEs
31+
.idea
32+
33+
# Finder (MacOS) folder config
34+
.DS_Store

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# react-exo-hooks
2+
3+
To install dependencies:
4+
5+
```bash
6+
bun install
7+
```
8+
9+
To run:
10+
11+
```bash
12+
bun run index.ts
13+
```
14+
15+
This project was created using `bun init` in bun v1.2.17. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

bun.lock

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"lockfileVersion": 1,
3+
"workspaces": {
4+
"": {
5+
"name": "react-exo-hooks",
6+
"dependencies": {
7+
"react": "^19.1.0",
8+
},
9+
"devDependencies": {
10+
"@types/bun": "latest",
11+
},
12+
"peerDependencies": {
13+
"typescript": "^5",
14+
},
15+
},
16+
},
17+
"packages": {
18+
"@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="],
19+
20+
"@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
21+
22+
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
23+
24+
"bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
25+
26+
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
27+
28+
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
29+
30+
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
31+
32+
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
33+
}
34+
}

hooks/use-map.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { type SetStateAction, useState, useEffect } from 'react'
2+
3+
/**
4+
* This is a set that causes rerenders on updates
5+
* @note Effects and memos that use this set should also listen for its signal: `+INSTANCE`
6+
*/
7+
export class StatefulMap<K, T> extends Map<K, 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<StatefulMap<K, 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?: Map<K, T> | Array<[K, T]>, dispatchSignal?: StatefulMap<K, T>['_dispatchSignal']) {
21+
super(initial)
22+
this._signal = 0
23+
this._dispatchSignal = dispatchSignal
24+
}
25+
26+
/**
27+
* Set the redefine dispatch
28+
* @private
29+
* @param callback The function
30+
*/
31+
_setRedefine (callback: StatefulMap<K, T>['_dispatchRedefine']): void {
32+
this._dispatchRedefine = callback
33+
}
34+
35+
/**
36+
* Force a signal update
37+
*/
38+
forceUpdate (): void {
39+
this._dispatchSignal?.(++this._signal)
40+
}
41+
42+
/**
43+
* Set the instance to an entirely new instance
44+
* @param value The new instance
45+
* @returns The new instance
46+
* @throws {Error} If no redefinition callback is defined
47+
*/
48+
reset (value: Map<K, T>): Map<K, T> {
49+
if (!this._dispatchRedefine) throw new Error('Cannot redefine Set. No redefine callback set.')
50+
const instance = new StatefulMap(value, this._dispatchSignal)
51+
instance._signal = this._signal
52+
53+
this._dispatchRedefine(instance)
54+
instance._dispatchSignal?.(++instance._signal)
55+
56+
return instance
57+
}
58+
59+
/**
60+
* @override
61+
*/
62+
override set (key: K, value: T): this {
63+
const old = super.get(key)
64+
const newKey = !this.has(key)
65+
super.set(key, value)
66+
if (newKey || old !== value) this._dispatchSignal?.(++this._signal)
67+
return this
68+
}
69+
70+
/**
71+
* Bulk set an array of items
72+
* @note Always rerenders
73+
* @param items An array of items
74+
* @param keyFn Either the name of a property of each item or a function that returns the key for each item
75+
* @returns this
76+
*/
77+
bulkSet<U extends K & keyof T> (items: T[], keyFn: U | ((i: T) => U)): this {
78+
for (const item of items) {
79+
const key = typeof keyFn === 'function' ? keyFn(item) : item[keyFn]
80+
81+
super.set(key as K, item)
82+
}
83+
this._dispatchSignal?.(++this._signal)
84+
85+
return this
86+
}
87+
88+
/**
89+
* @override
90+
*/
91+
override delete (key: K): boolean {
92+
const returnValue = super.delete(key)
93+
if (returnValue) this._dispatchSignal?.(++this._signal)
94+
return returnValue
95+
}
96+
97+
/**
98+
* @override
99+
*/
100+
override clear (): void {
101+
super.clear()
102+
this._dispatchSignal?.(this._signal = 0)
103+
}
104+
105+
/**
106+
* Returns the set's signal. Used for effects and memos that use this set
107+
* @returns The numeric signal
108+
*/
109+
override valueOf (): number {
110+
return this._signal
111+
}
112+
}
113+
114+
/**
115+
* Use a stately set
116+
* @note Any effects or memos that use this set should also listen for its signal (`+INSTANCE`)
117+
* @param initial The initial set value
118+
* @returns The stately set
119+
*/
120+
export function useMap<K, T> (initial?: Map<K, T> | Array<[K, T]>): StatefulMap<K, T> {
121+
const [, setSignal] = useState(Array.isArray(initial) ? initial.length : initial?.size ?? 0)
122+
const [map, setMap] = useState(new StatefulMap(initial, setSignal))
123+
124+
useEffect(() => map._setRedefine(setMap), [map])
125+
126+
return map
127+
}

index.ts

Whitespace-only changes.

package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "react-exo-hooks",
3+
"module": "index.ts",
4+
"type": "module",
5+
"devDependencies": {
6+
"@types/bun": "latest"
7+
},
8+
"peerDependencies": {
9+
"typescript": "^5"
10+
},
11+
"dependencies": {
12+
"react": "^19.1.0"
13+
}
14+
}

tsconfig.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"compilerOptions": {
3+
// Environment setup & latest features
4+
"lib": ["ESNext"],
5+
"target": "ESNext",
6+
"module": "Preserve",
7+
"moduleDetection": "force",
8+
"jsx": "react-jsx",
9+
"allowJs": true,
10+
11+
// Bundler mode
12+
"moduleResolution": "bundler",
13+
"allowImportingTsExtensions": true,
14+
"verbatimModuleSyntax": true,
15+
"noEmit": true,
16+
17+
// Best practices
18+
"strict": true,
19+
"skipLibCheck": true,
20+
"noFallthroughCasesInSwitch": true,
21+
"noUncheckedIndexedAccess": true,
22+
"noImplicitOverride": true,
23+
24+
// Some stricter flags (disabled by default)
25+
"noUnusedLocals": false,
26+
"noUnusedParameters": false,
27+
"noPropertyAccessFromIndexSignature": false
28+
}
29+
}

0 commit comments

Comments
 (0)