@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.
@@ -1,6 +1,6 @@
1
1
  // @flow
2
2
  //
3
- // Running a promise from a component.
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
- // Reading the browser, safely on a server.
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
- // Watching a node.
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 "./internal/async.js";
53
+ export type { Async } from "./async.js";
25
54
 
26
- export { useAsync } from "./internal/async.js";
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 "./internal/lifecycle.js";
64
+ } from "./lifecycle.js";
36
65
  export {
37
66
  useDebouncedCallback,
38
67
  useDebouncedValue,
39
68
  useInterval,
40
69
  useThrottledCallback,
41
70
  useTimeout,
42
- } from "./internal/timing.js";
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 "./internal/browser.js";
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 "./internal/element.js";
60
- export { useCounter, useStorage, useToggle } from "./internal/state.js";
88
+ } from "./dom.js";
89
+ export { useCounter, useStorage, useToggle } from "./state.js";
@@ -1,6 +1,6 @@
1
1
  // @flow
2
2
  //
3
- // The hooks everything else is built out of.
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.2",
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
- "index.js",
18
- "internal"
23
+ "*.js"
19
24
  ],
20
25
  "dependencies": {
21
- "@uniflowed/react": "0.0.0-alpha.2"
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
- // State with a shape.
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
- // Timers that stop when the component does.
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