@uniflowed/hooks 0.0.0-alpha.2 → 0.0.0-alpha.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/{internal/async.js → async.js} +14 -1
- package/{internal/browser.js → browser.js} +15 -1
- package/{internal/element.js → dom.js} +18 -1
- package/index.js +36 -7
- package/{internal/lifecycle.js → lifecycle.js} +15 -1
- package/package.json +10 -5
- package/{internal/state.js → state.js} +15 -1
- package/{internal/timing.js → timing.js} +13 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// `@uniflowed/hooks/async`: running a promise from a component.
|
|
4
4
|
//
|
|
5
5
|
// Two bugs a hand-written version has, and only one of them is a warning:
|
|
6
6
|
// setting state after the component has gone, and a slow first request
|
|
@@ -12,6 +12,19 @@
|
|
|
12
12
|
// because React runs its cleanup before running it again. That is the shape
|
|
13
13
|
// React's own documentation uses, and it means there is no "latest" anything
|
|
14
14
|
// to keep in a ref and no generation counter to keep in step.
|
|
15
|
+
//
|
|
16
|
+
// # What belongs in this module
|
|
17
|
+
//
|
|
18
|
+
// A hook that starts one call and holds its pending, resolved and failed
|
|
19
|
+
// states. One file for one hook, because the subject is neither the
|
|
20
|
+
// component's life nor a timer nor a DOM node, and hiding it inside one of
|
|
21
|
+
// those would make all three harder to name.
|
|
22
|
+
//
|
|
23
|
+
// Deliberately not here, and not in this package at all: caching, retries,
|
|
24
|
+
// deduplication between components, invalidation. Those are
|
|
25
|
+
// `@uniflowed/query`'s, and the boundary is what keeps `useAsync` small enough
|
|
26
|
+
// to read in one sitting. When a caller needs a cache they should change
|
|
27
|
+
// packages, not discover that this hook grew one.
|
|
15
28
|
|
|
16
29
|
import { useCallback, useEffect, useState } from "@uniflowed/react";
|
|
17
30
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// `@uniflowed/hooks/browser`: reading the environment, safely on a server.
|
|
4
4
|
//
|
|
5
5
|
// uf prerenders every static route, so each of these runs once where there is
|
|
6
6
|
// no `window`. `useSyncExternalStore` is what makes that correct rather than
|
|
@@ -9,6 +9,20 @@
|
|
|
9
9
|
// check happened to fall through to. It also means React reads the value at
|
|
10
10
|
// the moment it commits, which is what stops a media query changing between
|
|
11
11
|
// render and paint from tearing.
|
|
12
|
+
//
|
|
13
|
+
// # What belongs in this module
|
|
14
|
+
//
|
|
15
|
+
// A reading of the one browser the page is in: its size, its connection, its
|
|
16
|
+
// visibility, the preferences the reader set. There is exactly one answer at a
|
|
17
|
+
// time, nobody has to pass anything in to ask, and a server has no answer at
|
|
18
|
+
// all — which is why every hook here either takes a server value from the
|
|
19
|
+
// caller or states an honest default, and why every one of them is a
|
|
20
|
+
// `useSyncExternalStore` rather than an effect that sets state.
|
|
21
|
+
//
|
|
22
|
+
// Not here: anything about a specific element, which needs a ref and lives in
|
|
23
|
+
// `dom.js`. `useDocumentVisible` is the closest call in the package and stays
|
|
24
|
+
// here, because the document is the environment rather than a node a caller
|
|
25
|
+
// chose.
|
|
12
26
|
|
|
13
27
|
import { useCallback, useSyncExternalStore } from "@uniflowed/react";
|
|
14
28
|
|
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// `@uniflowed/hooks/dom`: watching one node.
|
|
4
4
|
//
|
|
5
5
|
// Each of these takes a ref rather than returning one, so a component can put
|
|
6
6
|
// several on the same element and can hand the ref to something else as well.
|
|
7
7
|
// The listener is attached in a layout effect, so it is in place before the
|
|
8
8
|
// browser paints — a click that lands in the same frame as the mount is a real
|
|
9
9
|
// case on a touch screen.
|
|
10
|
+
//
|
|
11
|
+
// # What belongs in this module
|
|
12
|
+
//
|
|
13
|
+
// A hook whose subject is a particular element the caller is holding: listen
|
|
14
|
+
// to it, measure it, notice a pointer over it, notice it entering the
|
|
15
|
+
// viewport. The signature is the giveaway — if it takes a ref, it is here.
|
|
16
|
+
//
|
|
17
|
+
// The neighbour it is most often confused with is `browser.js`, which reads
|
|
18
|
+
// the ambient environment: the window's size, whether the document is visible,
|
|
19
|
+
// what the reader's media queries say. Those need no ref because there is only
|
|
20
|
+
// one of the thing they read, and they are built on `useSyncExternalStore`
|
|
21
|
+
// with a stated server value. These need a ref because there are as many
|
|
22
|
+
// answers as there are elements, and they are built on effects because there
|
|
23
|
+
// is nothing to read until one is mounted.
|
|
24
|
+
//
|
|
25
|
+
// The file was called `element.js`, which named the argument rather than the
|
|
26
|
+
// job and left `browser.js` looking like its opposite when it is its sibling.
|
|
10
27
|
|
|
11
28
|
import { useEffect, useRef, useState } from "@uniflowed/react";
|
|
12
29
|
|
package/index.js
CHANGED
|
@@ -20,10 +20,39 @@
|
|
|
20
20
|
// Where there is no honest default, the caller supplies one: a page that hides
|
|
21
21
|
// its sidebar under 48rem wants `false` on the server and one that renders a
|
|
22
22
|
// mobile menu wants `true`, and a library cannot know which.
|
|
23
|
+
//
|
|
24
|
+
// # How the package is laid out
|
|
25
|
+
//
|
|
26
|
+
// Six modules beside this one, split by what a hook's subject is — because
|
|
27
|
+
// that is the question a reader looking for one actually asks:
|
|
28
|
+
//
|
|
29
|
+
// - `lifecycle.js` — the component itself: mounted, previous, run once.
|
|
30
|
+
// - `state.js` — a value the component owns, with the operations that suit it.
|
|
31
|
+
// - `timing.js` — when something runs: intervals, timeouts, debounce,
|
|
32
|
+
// throttle.
|
|
33
|
+
// - `async.js` — one promise, and the stale-response bug.
|
|
34
|
+
// - `browser.js` — the ambient environment: viewport, connection, preferences.
|
|
35
|
+
// - `dom.js` — one element the caller holds a ref to: listen, measure,
|
|
36
|
+
// observe.
|
|
37
|
+
//
|
|
38
|
+
// The two that are easiest to confuse are the last two, so each says so in its
|
|
39
|
+
// own header: `browser.js` needs no ref because there is one browser, and
|
|
40
|
+
// `dom.js` needs one because there are as many answers as there are elements.
|
|
41
|
+
//
|
|
42
|
+
// They sit here rather than under an `internal/`, and each has its own
|
|
43
|
+
// subpath. Every name in them is exported from this file, so calling them
|
|
44
|
+
// internal would have described nothing true, and it cost a reader a directory
|
|
45
|
+
// hop to reach the first line of code. `internal/` is for a module consumers
|
|
46
|
+
// must not reach; this package has none.
|
|
47
|
+
//
|
|
48
|
+
// `lifecycle.js` is the one the others import — `timing.js`, `state.js` and
|
|
49
|
+
// `dom.js` all want `useStableCallback` — and it is still a subject rather
|
|
50
|
+
// than a bag of shared helpers. A hook goes there because it is about the
|
|
51
|
+
// component's life, not because more than one file wanted it.
|
|
23
52
|
|
|
24
|
-
export type { Async } from "./
|
|
53
|
+
export type { Async } from "./async.js";
|
|
25
54
|
|
|
26
|
-
export { useAsync } from "./
|
|
55
|
+
export { useAsync } from "./async.js";
|
|
27
56
|
export {
|
|
28
57
|
useIsomorphicLayoutEffect,
|
|
29
58
|
useMount,
|
|
@@ -32,14 +61,14 @@ export {
|
|
|
32
61
|
useRerender,
|
|
33
62
|
useStableCallback,
|
|
34
63
|
useUnmount,
|
|
35
|
-
} from "./
|
|
64
|
+
} from "./lifecycle.js";
|
|
36
65
|
export {
|
|
37
66
|
useDebouncedCallback,
|
|
38
67
|
useDebouncedValue,
|
|
39
68
|
useInterval,
|
|
40
69
|
useThrottledCallback,
|
|
41
70
|
useTimeout,
|
|
42
|
-
} from "./
|
|
71
|
+
} from "./timing.js";
|
|
43
72
|
export {
|
|
44
73
|
useDocumentVisible,
|
|
45
74
|
useMediaQuery,
|
|
@@ -47,7 +76,7 @@ export {
|
|
|
47
76
|
usePrefersReducedMotion,
|
|
48
77
|
usePreferredColorScheme,
|
|
49
78
|
useWindowSize,
|
|
50
|
-
} from "./
|
|
79
|
+
} from "./browser.js";
|
|
51
80
|
export {
|
|
52
81
|
useClickOutside,
|
|
53
82
|
useElementRef,
|
|
@@ -56,5 +85,5 @@ export {
|
|
|
56
85
|
useFocusWithin,
|
|
57
86
|
useHover,
|
|
58
87
|
useIntersecting,
|
|
59
|
-
} from "./
|
|
60
|
-
export { useCounter, useStorage, useToggle } from "./
|
|
88
|
+
} from "./dom.js";
|
|
89
|
+
export { useCounter, useStorage, useToggle } from "./state.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// `@uniflowed/hooks/lifecycle`: where a component is in its life.
|
|
4
4
|
//
|
|
5
5
|
// The one that matters most is `useStableCallback`. A callback recreated every
|
|
6
6
|
// render is the single most common cause of a React performance problem and of
|
|
@@ -8,6 +8,20 @@
|
|
|
8
8
|
// and the usual fix, listing the callback in a dependency array, spreads the
|
|
9
9
|
// problem to every hook that takes it. A stable identity that always calls the
|
|
10
10
|
// latest closure fixes it once.
|
|
11
|
+
//
|
|
12
|
+
// # What belongs in this module
|
|
13
|
+
//
|
|
14
|
+
// A hook whose subject is the component itself: has it mounted, is it still
|
|
15
|
+
// mounted, what did it render last time, run this once, run this on the way
|
|
16
|
+
// out, and the two effect-shaped primitives the rest of the package needs
|
|
17
|
+
// (`useIsomorphicLayoutEffect`, `useStableCallback`). None of them reads
|
|
18
|
+
// anything outside React.
|
|
19
|
+
//
|
|
20
|
+
// It is also the one module here the others import, which is a consequence and
|
|
21
|
+
// not the reason: `timing.js`, `state.js` and `dom.js` all need a stable
|
|
22
|
+
// callback. That does not make this a "utils" or a "common" — it has a subject
|
|
23
|
+
// of its own, and a hook lands here because it is about the component's life,
|
|
24
|
+
// never because two other files happened to want it.
|
|
11
25
|
|
|
12
26
|
import {
|
|
13
27
|
useCallback,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/hooks",
|
|
3
|
-
"version": "0.0.0-alpha.
|
|
3
|
+
"version": "0.0.0-alpha.4",
|
|
4
4
|
"description": "The React hooks an application writes anyway, prerender-safe, part of the Unified Toolchain for Flow.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -11,14 +11,19 @@
|
|
|
11
11
|
"directory": "packages/hooks"
|
|
12
12
|
},
|
|
13
13
|
"exports": {
|
|
14
|
-
".": "./index.js"
|
|
14
|
+
".": "./index.js",
|
|
15
|
+
"./async": "./async.js",
|
|
16
|
+
"./browser": "./browser.js",
|
|
17
|
+
"./dom": "./dom.js",
|
|
18
|
+
"./lifecycle": "./lifecycle.js",
|
|
19
|
+
"./state": "./state.js",
|
|
20
|
+
"./timing": "./timing.js"
|
|
15
21
|
},
|
|
16
22
|
"files": [
|
|
17
|
-
"
|
|
18
|
-
"internal"
|
|
23
|
+
"*.js"
|
|
19
24
|
],
|
|
20
25
|
"dependencies": {
|
|
21
|
-
"@uniflowed/react": "0.0.0-alpha.
|
|
26
|
+
"@uniflowed/react": "0.0.0-alpha.4"
|
|
22
27
|
},
|
|
23
28
|
"peerDependencies": {
|
|
24
29
|
"react": ">=19"
|
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// `@uniflowed/hooks/state`: state with a shape.
|
|
4
4
|
//
|
|
5
5
|
// `useStorage` is the one worth reading. Persisted state has three problems a
|
|
6
6
|
// `useState` plus a `useEffect` does not solve: the first render on a
|
|
7
7
|
// prerendered page has no storage to read, two components using the same key
|
|
8
8
|
// must agree, and another tab writing the key should be seen. All three are
|
|
9
9
|
// what `useSyncExternalStore` is for.
|
|
10
|
+
//
|
|
11
|
+
// # What belongs in this module
|
|
12
|
+
//
|
|
13
|
+
// A `useState` a component would otherwise write out by hand, returned as the
|
|
14
|
+
// operations that make sense on it rather than as a setter: a boolean with
|
|
15
|
+
// `toggle`, a number with `increment` and a clamp, a value that survives a
|
|
16
|
+
// reload. The test is that the hook owns the value and hands back a small API
|
|
17
|
+
// over it.
|
|
18
|
+
//
|
|
19
|
+
// Not here: shared application state. An atom two routes both read is
|
|
20
|
+
// `@uniflowed/state`'s, and a value derived from a server response is
|
|
21
|
+
// `@uniflowed/query`'s. Everything in this file is local to one component —
|
|
22
|
+
// `useStorage` reaches outside only to persist, and only under a key the
|
|
23
|
+
// caller named.
|
|
10
24
|
|
|
11
25
|
import { useCallback, useMemo, useState, useSyncExternalStore } from "@uniflowed/react";
|
|
12
26
|
|
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// `@uniflowed/hooks/timing`: timers that stop when the component does.
|
|
4
4
|
//
|
|
5
5
|
// Every one of these exists because the hand-written version leaks: a
|
|
6
6
|
// `setInterval` in a `useEffect` whose dependency array includes the callback
|
|
7
7
|
// is torn down and restarted on every render, and one without the callback in
|
|
8
8
|
// the array calls a stale closure forever. `useStableCallback` removes the
|
|
9
9
|
// choice — the timer is set once and always calls the current body.
|
|
10
|
+
//
|
|
11
|
+
// # What belongs in this module
|
|
12
|
+
//
|
|
13
|
+
// A hook whose subject is *when* something runs: on a schedule, after a wait,
|
|
14
|
+
// no more often than some rate. Every one of them owns a handle that has to be
|
|
15
|
+
// cleared, and the cleanup is the reason the hook exists rather than a detail
|
|
16
|
+
// of it.
|
|
17
|
+
//
|
|
18
|
+
// Not here: `useMount` and `useUnmount`, which are about the component's life
|
|
19
|
+
// rather than a clock, and live in `lifecycle.js`; and rendering a time, which
|
|
20
|
+
// is `@uniflowed/web`'s `Time` and is a formatting problem, not a scheduling
|
|
21
|
+
// one.
|
|
10
22
|
|
|
11
23
|
import { useEffect, useRef, useState } from "@uniflowed/react";
|
|
12
24
|
|