react-snapshot-bridge 1.1.2 → 1.2.1
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/README.md +43 -20
- package/dist/index.cjs +17 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -3
- package/dist/index.d.ts +23 -3
- package/dist/index.js +17 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,13 +46,21 @@ Peer dependency: `react >= 16.3` (when `getSnapshotBeforeUpdate` was introduced)
|
|
|
46
46
|
import { useRef, useState } from "react";
|
|
47
47
|
import { SnapshotBeforeUpdate } from "react-snapshot-bridge";
|
|
48
48
|
|
|
49
|
+
type Message = { id: number; text: string };
|
|
50
|
+
|
|
49
51
|
function ChatList() {
|
|
50
52
|
const scrollerRef = useRef<HTMLDivElement>(null);
|
|
51
|
-
const
|
|
53
|
+
const nextId = useRef(1);
|
|
54
|
+
const [messages, setMessages] = useState<Message[]>([{ id: 0, text: "hi" }]);
|
|
52
55
|
|
|
53
56
|
return (
|
|
54
57
|
<>
|
|
55
|
-
<button
|
|
58
|
+
<button
|
|
59
|
+
onClick={() => {
|
|
60
|
+
const id = nextId.current++;
|
|
61
|
+
setMessages((m) => [{ id, text: "new message" }, ...m]);
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
56
64
|
Prepend
|
|
57
65
|
</button>
|
|
58
66
|
|
|
@@ -66,9 +74,12 @@ function ChatList() {
|
|
|
66
74
|
el.scrollTop = el.scrollHeight - prevDistanceFromBottom;
|
|
67
75
|
}}
|
|
68
76
|
>
|
|
69
|
-
<div
|
|
70
|
-
{
|
|
71
|
-
|
|
77
|
+
<div
|
|
78
|
+
ref={scrollerRef}
|
|
79
|
+
style={{ height: 200, overflow: "auto", overflowAnchor: "none" }}
|
|
80
|
+
>
|
|
81
|
+
{messages.map((msg) => (
|
|
82
|
+
<div key={msg.id}>{msg.text}</div>
|
|
72
83
|
))}
|
|
73
84
|
</div>
|
|
74
85
|
</SnapshotBeforeUpdate>
|
|
@@ -79,26 +90,32 @@ function ChatList() {
|
|
|
79
90
|
|
|
80
91
|
Without the wrapper, every prepend visually shifts the user's content down. With it, the visible content stays anchored.
|
|
81
92
|
|
|
93
|
+
Give each row a stable `id`. An index key makes React reuse the existing nodes from the top, so a prepend is reconciled like an append and this anchor no longer matches what the reader is looking at.
|
|
94
|
+
|
|
95
|
+
`overflow-anchor: none` turns off the browser's own scroll anchoring. Without that, Chrome and Safari often keep the same row in view by themselves, and the jump this wrapper corrects stays hidden.
|
|
96
|
+
|
|
82
97
|
## Capturing multiple values
|
|
83
98
|
|
|
84
99
|
`capture` returns a single value, but that value can be **any shape** — including an object. This is the recommended way to forward multiple pieces of state in one commit, and it keeps the API surface minimal and type-safe.
|
|
85
100
|
|
|
86
101
|
The example below combines two ideas at once:
|
|
87
102
|
|
|
88
|
-
1. Capture
|
|
103
|
+
1. Capture the scroll anchor (`anchorFromBottom`, the same `scrollHeight - scrollTop` value as the quick start) and whether the reader was already at the bottom.
|
|
89
104
|
2. Track the **previous** message count with a ref (synced at the end of `apply`), read the **current** `messages` in `apply` via closure, and read DOM in `capture` before the commit mutates it.
|
|
90
105
|
|
|
91
106
|
```tsx
|
|
92
|
-
import { useRef
|
|
107
|
+
import { useRef } from "react";
|
|
93
108
|
import { SnapshotBeforeUpdate } from "react-snapshot-bridge";
|
|
94
109
|
|
|
110
|
+
type Message = { id: number; text: string };
|
|
111
|
+
|
|
95
112
|
type Snap = {
|
|
96
|
-
|
|
113
|
+
anchorFromBottom: number;
|
|
97
114
|
wasAtBottom: boolean;
|
|
98
115
|
previousMessageCount: number;
|
|
99
116
|
};
|
|
100
117
|
|
|
101
|
-
function ChatList({ messages }: { messages:
|
|
118
|
+
function ChatList({ messages }: { messages: Message[] }) {
|
|
102
119
|
const scrollerRef = useRef<HTMLDivElement>(null);
|
|
103
120
|
const prevMessageCountRef = useRef(messages.length);
|
|
104
121
|
|
|
@@ -106,32 +123,35 @@ function ChatList({ messages }: { messages: string[] }) {
|
|
|
106
123
|
<SnapshotBeforeUpdate<Snap>
|
|
107
124
|
capture={() => {
|
|
108
125
|
const el = scrollerRef.current!;
|
|
109
|
-
const
|
|
110
|
-
|
|
126
|
+
const anchorFromBottom = el.scrollHeight - el.scrollTop;
|
|
127
|
+
const gapBelowViewport = anchorFromBottom - el.clientHeight;
|
|
111
128
|
return {
|
|
112
|
-
|
|
113
|
-
wasAtBottom:
|
|
129
|
+
anchorFromBottom,
|
|
130
|
+
wasAtBottom: gapBelowViewport < 16,
|
|
114
131
|
// Still the count from the end of the last `apply` (or initial render).
|
|
115
132
|
previousMessageCount: prevMessageCountRef.current,
|
|
116
133
|
};
|
|
117
134
|
}}
|
|
118
|
-
apply={({
|
|
135
|
+
apply={({ anchorFromBottom, wasAtBottom, previousMessageCount }) => {
|
|
119
136
|
const el = scrollerRef.current!;
|
|
120
137
|
const grewAtTop = messages.length > previousMessageCount;
|
|
121
138
|
|
|
122
139
|
if (wasAtBottom) {
|
|
123
140
|
el.scrollTop = el.scrollHeight; // stick to bottom for new arrivals
|
|
124
141
|
} else if (grewAtTop) {
|
|
125
|
-
el.scrollTop = el.scrollHeight -
|
|
142
|
+
el.scrollTop = el.scrollHeight - anchorFromBottom; // anchor existing content
|
|
126
143
|
}
|
|
127
144
|
|
|
128
145
|
// Next `capture` reads this as the "previous" count for that commit.
|
|
129
146
|
prevMessageCountRef.current = messages.length;
|
|
130
147
|
}}
|
|
131
148
|
>
|
|
132
|
-
<div
|
|
133
|
-
{
|
|
134
|
-
|
|
149
|
+
<div
|
|
150
|
+
ref={scrollerRef}
|
|
151
|
+
style={{ height: 200, overflow: "auto", overflowAnchor: "none" }}
|
|
152
|
+
>
|
|
153
|
+
{messages.map((msg) => (
|
|
154
|
+
<div key={msg.id}>{msg.text}</div>
|
|
135
155
|
))}
|
|
136
156
|
</div>
|
|
137
157
|
</SnapshotBeforeUpdate>
|
|
@@ -143,13 +163,14 @@ function ChatList({ messages }: { messages: string[] }) {
|
|
|
143
163
|
|
|
144
164
|
## API
|
|
145
165
|
|
|
146
|
-
### `<SnapshotBeforeUpdate<T> capture apply enabled?>{children?}</SnapshotBeforeUpdate>`
|
|
166
|
+
### `<SnapshotBeforeUpdate<T> capture apply enabled? deps?>{children?}</SnapshotBeforeUpdate>`
|
|
147
167
|
|
|
148
168
|
| Prop | Type | Required | Description |
|
|
149
169
|
| ---------- | ----------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
150
|
-
| `capture` | `() => T` | yes | Called synchronously **before** DOM mutation. Return value is forwarded to `apply`.
|
|
170
|
+
| `capture` | `() => T` | yes | Called synchronously **before** DOM mutation. Return value, including `undefined`, is forwarded to `apply`. |
|
|
151
171
|
| `apply` | `(snapshot: T) => void` | yes | Called synchronously **after** DOM mutation, before paint. Receives the value from `capture`. |
|
|
152
172
|
| `enabled` | `boolean` | no | Defaults to `true`. When `false`, both `capture` and `apply` are skipped for that commit. Missed updates are not replayed. |
|
|
173
|
+
| `deps` | `readonly unknown[]` | no | Optional dependency array, compared shallowly (`Object.is`) against the previous commit. When provided, both callbacks run only on commits where at least one entry changed. When omitted, they run on every update. |
|
|
153
174
|
| `children` | `React.ReactNode` | no | Optional. When provided, rendered as-is (wrapper pattern). When omitted, renders nothing. |
|
|
154
175
|
|
|
155
176
|
The generic `T` is inferred from `capture`'s return type — you usually don't need to specify it.
|
|
@@ -224,8 +245,10 @@ The captured value is forwarded to `componentDidUpdate`, which calls `apply`. Fr
|
|
|
224
245
|
- **Updates run before DOM mutation for that commit.** After mount, whenever the bridge commits an update, `capture` runs in the **before mutation** phase: before the DOM reflects that commit's output and before the browser paints that update. That is the timing slot this library occupies (see [How it works](#how-it-works)).
|
|
225
246
|
- **Requires a re-render in the right place.** If a `React.memo` (or `shouldComponentUpdate`) ancestor short-circuits the render, or the bridge is placed as a sibling of a different component that updates on its own, neither callback will fire. See [Where to place it](#where-to-place-it).
|
|
226
247
|
- **`enabled={false}` does not buffer updates.** Updates that occur while disabled are silently dropped. There is no "catch up" call when you re-enable the bridge — the next regular update is what triggers `capture`/`apply` again.
|
|
248
|
+
- **`deps={[]}` differs from `useEffect`.** Because an empty array never changes and this lifecycle never runs on mount, `deps={[]}` means "never run after mount" — not "run once on mount" as it would with `useEffect`. Use a populated `deps` array (e.g. `[messages.length]`) to gate by meaningful changes.
|
|
227
249
|
- **Concurrent rendering safe.** React 18+ concurrent features may discard render results, but the commit phase itself is synchronous, so `capture`/`apply` always run as a paired set.
|
|
228
250
|
- **SSR safe.** Lifecycle methods don't run during `renderToString` / `renderToPipeableStream`, so the bridge is a no-op on the server.
|
|
251
|
+
- **Client Component.** The package entry starts with `'use client'`. In the Next.js App Router, render it from a Client Component. `capture` and `apply` are functions, so a Server Component cannot pass them into this wrapper.
|
|
229
252
|
- **React 19.** Class components and `getSnapshotBeforeUpdate` are not deprecated in React 19. This library remains compatible.
|
|
230
253
|
|
|
231
254
|
## License
|
package/dist/index.cjs
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
'use strict';
|
|
2
3
|
|
|
3
4
|
var react = require('react');
|
|
4
5
|
|
|
6
|
+
// src/_utils/checkIsDepsEqual.ts
|
|
7
|
+
function checkIsDepsEqual(prev, next) {
|
|
8
|
+
if (prev === void 0 || next === void 0) return false;
|
|
9
|
+
if (prev.length !== next.length) return false;
|
|
10
|
+
for (let i = 0; i < prev.length; i++) {
|
|
11
|
+
if (!Object.is(prev[i], next[i])) return false;
|
|
12
|
+
}
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
var checkIsDepsEqual_default = checkIsDepsEqual;
|
|
16
|
+
|
|
5
17
|
// src/SnapshotBeforeUpdate.tsx
|
|
6
18
|
var SKIP = /* @__PURE__ */ Symbol("react-snapshot-bridge-skip");
|
|
7
19
|
var SnapshotBeforeUpdate = class extends react.Component {
|
|
8
|
-
getSnapshotBeforeUpdate() {
|
|
9
|
-
if (this.props.enabled === false)
|
|
20
|
+
getSnapshotBeforeUpdate(prevProps) {
|
|
21
|
+
if (this.props.enabled === false) return SKIP;
|
|
22
|
+
if (this.props.deps !== void 0 && checkIsDepsEqual_default(prevProps.deps, this.props.deps))
|
|
10
23
|
return SKIP;
|
|
11
|
-
}
|
|
12
|
-
return this.props.capture();
|
|
24
|
+
return { value: this.props.capture() };
|
|
13
25
|
}
|
|
14
26
|
componentDidUpdate(_prevProps, _prevState, snapshot) {
|
|
15
|
-
if (this.props.enabled === false) return;
|
|
16
27
|
if (snapshot === SKIP) return;
|
|
17
|
-
this.props.apply(snapshot);
|
|
28
|
+
this.props.apply(snapshot.value);
|
|
18
29
|
}
|
|
19
30
|
render() {
|
|
20
31
|
return this.props.children ?? null;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/SnapshotBeforeUpdate.tsx"],"names":["Component"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"sources":["../src/_utils/checkIsDepsEqual.ts","../src/SnapshotBeforeUpdate.tsx"],"names":["Component"],"mappings":";;;;;AAAA,SAAS,gBAAA,CACP,MACA,IAAA,EACS;AACT,EAAA,IAAI,IAAA,KAAS,MAAA,IAAa,IAAA,KAAS,MAAA,EAAW,OAAO,KAAA;AACrD,EAAA,IAAI,IAAA,CAAK,MAAA,KAAW,IAAA,CAAK,MAAA,EAAQ,OAAO,KAAA;AACxC,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,IAAA,CAAK,QAAQ,CAAA,EAAA,EAAK;AACpC,IAAA,IAAI,CAAC,MAAA,CAAO,EAAA,CAAG,IAAA,CAAK,CAAC,GAAG,IAAA,CAAK,CAAC,CAAC,CAAA,EAAG,OAAO,KAAA;AAAA,EAC3C;AACA,EAAA,OAAO,IAAA;AACT;AAEA,IAAO,wBAAA,GAAQ,gBAAA;;;ACHf,IAAM,IAAA,0BAAc,4BAA4B,CAAA;AA2DzC,IAAM,oBAAA,GAAN,cAAgDA,eAAA,CAIrD;AAAA,EACS,wBACP,SAAA,EACyB;AACzB,IAAA,IAAI,IAAA,CAAK,KAAA,CAAM,OAAA,KAAY,KAAA,EAAO,OAAO,IAAA;AAEzC,IAAA,IAAI,IAAA,CAAK,MAAM,IAAA,KAAS,MAAA,IAAa,yBAAiB,SAAA,CAAU,IAAA,EAAM,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AACnF,MAAA,OAAO,IAAA;AAET,IAAA,OAAO,EAAE,KAAA,EAAO,IAAA,CAAK,KAAA,CAAM,SAAQ,EAAE;AAAA,EACvC;AAAA,EAES,kBAAA,CACP,UAAA,EACA,UAAA,EACA,QAAA,EACM;AACN,IAAA,IAAI,aAAa,IAAA,EAAM;AAEvB,IAAA,IAAA,CAAK,KAAA,CAAM,KAAA,CAAM,QAAA,CAAS,KAAK,CAAA;AAAA,EACjC;AAAA,EAES,MAAA,GAAoB;AAC3B,IAAA,OAAO,IAAA,CAAK,MAAM,QAAA,IAAY,IAAA;AAAA,EAChC;AACF","file":"index.cjs","sourcesContent":["function checkIsDepsEqual(\n prev: readonly unknown[] | undefined,\n next: readonly unknown[] | undefined,\n): boolean {\n if (prev === undefined || next === undefined) return false;\n if (prev.length !== next.length) return false;\n for (let i = 0; i < prev.length; i++) {\n if (!Object.is(prev[i], next[i])) return false;\n }\n return true;\n}\n\nexport default checkIsDepsEqual;\n","import type { ReactNode } from 'react';\nimport { Component } from 'react';\nimport checkIsDepsEqual from './_utils/checkIsDepsEqual';\n\ntype EmptyState = Record<string, never>;\n\n/**\n * Symbol used to indicate that the snapshot should be skipped.\n */\nconst SKIP = Symbol('react-snapshot-bridge-skip');\ntype Skip = typeof SKIP;\n\n/**\n * React logs an error when `getSnapshotBeforeUpdate` returns `undefined`.\n * Boxing the value lets `capture` return `undefined` and still forward it to `apply`.\n */\ntype BoxedSnapshot<T> = {\n readonly value: T;\n};\n\nexport interface SnapshotBeforeUpdateProps<T> {\n /**\n * Runs synchronously **before** the DOM is mutated for this commit.\n * Whatever you return here is forwarded to {@link SnapshotBeforeUpdateProps.apply}.\n * `undefined` is a valid snapshot and is forwarded unchanged.\n */\n capture: () => T;\n\n /**\n * Runs synchronously **after** the DOM is mutated, before the browser paints.\n * Receives the value previously returned from {@link SnapshotBeforeUpdateProps.capture}.\n */\n apply: (snapshot: T) => void;\n\n /**\n * When `false`, both `capture` and `apply` are skipped for that commit.\n * Evaluated against the **current** props at commit time, so toggling\n * `true` -> `false` skips both callbacks for that update, and `false` -> `true`\n * runs both as usual on the next update. Defaults to `true`.\n *\n * Updates that occur while disabled are not buffered or replayed later.\n */\n enabled?: boolean;\n\n /**\n * Optional dependency array, compared shallowly (`Object.is`) against the\n * previous commit. When provided, both `capture` and `apply` run only on the\n * commit where at least one entry changed; commits with unchanged `deps` are\n * skipped. When omitted, both callbacks run on every update (default).\n */\n deps?: readonly unknown[];\n\n /**\n * Optional children. When provided, this component acts as a wrapper and\n * renders its children. When omitted, it renders nothing and you can place\n * it as a sibling next to the elements you care about.\n */\n children?: ReactNode;\n}\n\n/**\n * Bridges React's class-only `getSnapshotBeforeUpdate` lifecycle into trees\n * built with function components.\n *\n * The component re-renders alongside its parent and uses the `getSnapshotBeforeUpdate`\n * / `componentDidUpdate` pair internally to invoke {@link SnapshotBeforeUpdateProps.capture}\n * before DOM mutation and {@link SnapshotBeforeUpdateProps.apply} immediately after.\n */\nexport class SnapshotBeforeUpdate<T = unknown> extends Component<\n SnapshotBeforeUpdateProps<T>,\n EmptyState,\n BoxedSnapshot<T> | Skip\n> {\n override getSnapshotBeforeUpdate(\n prevProps: Readonly<SnapshotBeforeUpdateProps<T>>,\n ): BoxedSnapshot<T> | Skip {\n if (this.props.enabled === false) return SKIP;\n\n if (this.props.deps !== undefined && checkIsDepsEqual(prevProps.deps, this.props.deps))\n return SKIP;\n\n return { value: this.props.capture() };\n }\n\n override componentDidUpdate(\n _prevProps: Readonly<SnapshotBeforeUpdateProps<T>>,\n _prevState: Readonly<EmptyState>,\n snapshot: BoxedSnapshot<T> | Skip,\n ): void {\n if (snapshot === SKIP) return;\n\n this.props.apply(snapshot.value);\n }\n\n override render(): ReactNode {\n return this.props.children ?? null;\n }\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { Component, ReactNode } from 'react';
|
|
2
2
|
|
|
3
3
|
type EmptyState = Record<string, never>;
|
|
4
|
+
/**
|
|
5
|
+
* Symbol used to indicate that the snapshot should be skipped.
|
|
6
|
+
*/
|
|
7
|
+
declare const SKIP: unique symbol;
|
|
8
|
+
type Skip = typeof SKIP;
|
|
9
|
+
/**
|
|
10
|
+
* React logs an error when `getSnapshotBeforeUpdate` returns `undefined`.
|
|
11
|
+
* Boxing the value lets `capture` return `undefined` and still forward it to `apply`.
|
|
12
|
+
*/
|
|
13
|
+
type BoxedSnapshot<T> = {
|
|
14
|
+
readonly value: T;
|
|
15
|
+
};
|
|
4
16
|
interface SnapshotBeforeUpdateProps<T> {
|
|
5
17
|
/**
|
|
6
18
|
* Runs synchronously **before** the DOM is mutated for this commit.
|
|
7
19
|
* Whatever you return here is forwarded to {@link SnapshotBeforeUpdateProps.apply}.
|
|
20
|
+
* `undefined` is a valid snapshot and is forwarded unchanged.
|
|
8
21
|
*/
|
|
9
22
|
capture: () => T;
|
|
10
23
|
/**
|
|
@@ -21,6 +34,13 @@ interface SnapshotBeforeUpdateProps<T> {
|
|
|
21
34
|
* Updates that occur while disabled are not buffered or replayed later.
|
|
22
35
|
*/
|
|
23
36
|
enabled?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Optional dependency array, compared shallowly (`Object.is`) against the
|
|
39
|
+
* previous commit. When provided, both `capture` and `apply` run only on the
|
|
40
|
+
* commit where at least one entry changed; commits with unchanged `deps` are
|
|
41
|
+
* skipped. When omitted, both callbacks run on every update (default).
|
|
42
|
+
*/
|
|
43
|
+
deps?: readonly unknown[];
|
|
24
44
|
/**
|
|
25
45
|
* Optional children. When provided, this component acts as a wrapper and
|
|
26
46
|
* renders its children. When omitted, it renders nothing and you can place
|
|
@@ -36,9 +56,9 @@ interface SnapshotBeforeUpdateProps<T> {
|
|
|
36
56
|
* / `componentDidUpdate` pair internally to invoke {@link SnapshotBeforeUpdateProps.capture}
|
|
37
57
|
* before DOM mutation and {@link SnapshotBeforeUpdateProps.apply} immediately after.
|
|
38
58
|
*/
|
|
39
|
-
declare class SnapshotBeforeUpdate<T = unknown> extends Component<SnapshotBeforeUpdateProps<T>, EmptyState> {
|
|
40
|
-
getSnapshotBeforeUpdate(): T;
|
|
41
|
-
componentDidUpdate(_prevProps: Readonly<SnapshotBeforeUpdateProps<T>>, _prevState: Readonly<EmptyState>, snapshot: T): void;
|
|
59
|
+
declare class SnapshotBeforeUpdate<T = unknown> extends Component<SnapshotBeforeUpdateProps<T>, EmptyState, BoxedSnapshot<T> | Skip> {
|
|
60
|
+
getSnapshotBeforeUpdate(prevProps: Readonly<SnapshotBeforeUpdateProps<T>>): BoxedSnapshot<T> | Skip;
|
|
61
|
+
componentDidUpdate(_prevProps: Readonly<SnapshotBeforeUpdateProps<T>>, _prevState: Readonly<EmptyState>, snapshot: BoxedSnapshot<T> | Skip): void;
|
|
42
62
|
render(): ReactNode;
|
|
43
63
|
}
|
|
44
64
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { Component, ReactNode } from 'react';
|
|
2
2
|
|
|
3
3
|
type EmptyState = Record<string, never>;
|
|
4
|
+
/**
|
|
5
|
+
* Symbol used to indicate that the snapshot should be skipped.
|
|
6
|
+
*/
|
|
7
|
+
declare const SKIP: unique symbol;
|
|
8
|
+
type Skip = typeof SKIP;
|
|
9
|
+
/**
|
|
10
|
+
* React logs an error when `getSnapshotBeforeUpdate` returns `undefined`.
|
|
11
|
+
* Boxing the value lets `capture` return `undefined` and still forward it to `apply`.
|
|
12
|
+
*/
|
|
13
|
+
type BoxedSnapshot<T> = {
|
|
14
|
+
readonly value: T;
|
|
15
|
+
};
|
|
4
16
|
interface SnapshotBeforeUpdateProps<T> {
|
|
5
17
|
/**
|
|
6
18
|
* Runs synchronously **before** the DOM is mutated for this commit.
|
|
7
19
|
* Whatever you return here is forwarded to {@link SnapshotBeforeUpdateProps.apply}.
|
|
20
|
+
* `undefined` is a valid snapshot and is forwarded unchanged.
|
|
8
21
|
*/
|
|
9
22
|
capture: () => T;
|
|
10
23
|
/**
|
|
@@ -21,6 +34,13 @@ interface SnapshotBeforeUpdateProps<T> {
|
|
|
21
34
|
* Updates that occur while disabled are not buffered or replayed later.
|
|
22
35
|
*/
|
|
23
36
|
enabled?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Optional dependency array, compared shallowly (`Object.is`) against the
|
|
39
|
+
* previous commit. When provided, both `capture` and `apply` run only on the
|
|
40
|
+
* commit where at least one entry changed; commits with unchanged `deps` are
|
|
41
|
+
* skipped. When omitted, both callbacks run on every update (default).
|
|
42
|
+
*/
|
|
43
|
+
deps?: readonly unknown[];
|
|
24
44
|
/**
|
|
25
45
|
* Optional children. When provided, this component acts as a wrapper and
|
|
26
46
|
* renders its children. When omitted, it renders nothing and you can place
|
|
@@ -36,9 +56,9 @@ interface SnapshotBeforeUpdateProps<T> {
|
|
|
36
56
|
* / `componentDidUpdate` pair internally to invoke {@link SnapshotBeforeUpdateProps.capture}
|
|
37
57
|
* before DOM mutation and {@link SnapshotBeforeUpdateProps.apply} immediately after.
|
|
38
58
|
*/
|
|
39
|
-
declare class SnapshotBeforeUpdate<T = unknown> extends Component<SnapshotBeforeUpdateProps<T>, EmptyState> {
|
|
40
|
-
getSnapshotBeforeUpdate(): T;
|
|
41
|
-
componentDidUpdate(_prevProps: Readonly<SnapshotBeforeUpdateProps<T>>, _prevState: Readonly<EmptyState>, snapshot: T): void;
|
|
59
|
+
declare class SnapshotBeforeUpdate<T = unknown> extends Component<SnapshotBeforeUpdateProps<T>, EmptyState, BoxedSnapshot<T> | Skip> {
|
|
60
|
+
getSnapshotBeforeUpdate(prevProps: Readonly<SnapshotBeforeUpdateProps<T>>): BoxedSnapshot<T> | Skip;
|
|
61
|
+
componentDidUpdate(_prevProps: Readonly<SnapshotBeforeUpdateProps<T>>, _prevState: Readonly<EmptyState>, snapshot: BoxedSnapshot<T> | Skip): void;
|
|
42
62
|
render(): ReactNode;
|
|
43
63
|
}
|
|
44
64
|
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { Component } from 'react';
|
|
2
3
|
|
|
4
|
+
// src/_utils/checkIsDepsEqual.ts
|
|
5
|
+
function checkIsDepsEqual(prev, next) {
|
|
6
|
+
if (prev === void 0 || next === void 0) return false;
|
|
7
|
+
if (prev.length !== next.length) return false;
|
|
8
|
+
for (let i = 0; i < prev.length; i++) {
|
|
9
|
+
if (!Object.is(prev[i], next[i])) return false;
|
|
10
|
+
}
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
var checkIsDepsEqual_default = checkIsDepsEqual;
|
|
14
|
+
|
|
3
15
|
// src/SnapshotBeforeUpdate.tsx
|
|
4
16
|
var SKIP = /* @__PURE__ */ Symbol("react-snapshot-bridge-skip");
|
|
5
17
|
var SnapshotBeforeUpdate = class extends Component {
|
|
6
|
-
getSnapshotBeforeUpdate() {
|
|
7
|
-
if (this.props.enabled === false)
|
|
18
|
+
getSnapshotBeforeUpdate(prevProps) {
|
|
19
|
+
if (this.props.enabled === false) return SKIP;
|
|
20
|
+
if (this.props.deps !== void 0 && checkIsDepsEqual_default(prevProps.deps, this.props.deps))
|
|
8
21
|
return SKIP;
|
|
9
|
-
}
|
|
10
|
-
return this.props.capture();
|
|
22
|
+
return { value: this.props.capture() };
|
|
11
23
|
}
|
|
12
24
|
componentDidUpdate(_prevProps, _prevState, snapshot) {
|
|
13
|
-
if (this.props.enabled === false) return;
|
|
14
25
|
if (snapshot === SKIP) return;
|
|
15
|
-
this.props.apply(snapshot);
|
|
26
|
+
this.props.apply(snapshot.value);
|
|
16
27
|
}
|
|
17
28
|
render() {
|
|
18
29
|
return this.props.children ?? null;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/SnapshotBeforeUpdate.tsx"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"sources":["../src/_utils/checkIsDepsEqual.ts","../src/SnapshotBeforeUpdate.tsx"],"names":[],"mappings":";;;AAAA,SAAS,gBAAA,CACP,MACA,IAAA,EACS;AACT,EAAA,IAAI,IAAA,KAAS,MAAA,IAAa,IAAA,KAAS,MAAA,EAAW,OAAO,KAAA;AACrD,EAAA,IAAI,IAAA,CAAK,MAAA,KAAW,IAAA,CAAK,MAAA,EAAQ,OAAO,KAAA;AACxC,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,IAAA,CAAK,QAAQ,CAAA,EAAA,EAAK;AACpC,IAAA,IAAI,CAAC,MAAA,CAAO,EAAA,CAAG,IAAA,CAAK,CAAC,GAAG,IAAA,CAAK,CAAC,CAAC,CAAA,EAAG,OAAO,KAAA;AAAA,EAC3C;AACA,EAAA,OAAO,IAAA;AACT;AAEA,IAAO,wBAAA,GAAQ,gBAAA;;;ACHf,IAAM,IAAA,0BAAc,4BAA4B,CAAA;AA2DzC,IAAM,oBAAA,GAAN,cAAgD,SAAA,CAIrD;AAAA,EACS,wBACP,SAAA,EACyB;AACzB,IAAA,IAAI,IAAA,CAAK,KAAA,CAAM,OAAA,KAAY,KAAA,EAAO,OAAO,IAAA;AAEzC,IAAA,IAAI,IAAA,CAAK,MAAM,IAAA,KAAS,MAAA,IAAa,yBAAiB,SAAA,CAAU,IAAA,EAAM,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AACnF,MAAA,OAAO,IAAA;AAET,IAAA,OAAO,EAAE,KAAA,EAAO,IAAA,CAAK,KAAA,CAAM,SAAQ,EAAE;AAAA,EACvC;AAAA,EAES,kBAAA,CACP,UAAA,EACA,UAAA,EACA,QAAA,EACM;AACN,IAAA,IAAI,aAAa,IAAA,EAAM;AAEvB,IAAA,IAAA,CAAK,KAAA,CAAM,KAAA,CAAM,QAAA,CAAS,KAAK,CAAA;AAAA,EACjC;AAAA,EAES,MAAA,GAAoB;AAC3B,IAAA,OAAO,IAAA,CAAK,MAAM,QAAA,IAAY,IAAA;AAAA,EAChC;AACF","file":"index.js","sourcesContent":["function checkIsDepsEqual(\n prev: readonly unknown[] | undefined,\n next: readonly unknown[] | undefined,\n): boolean {\n if (prev === undefined || next === undefined) return false;\n if (prev.length !== next.length) return false;\n for (let i = 0; i < prev.length; i++) {\n if (!Object.is(prev[i], next[i])) return false;\n }\n return true;\n}\n\nexport default checkIsDepsEqual;\n","import type { ReactNode } from 'react';\nimport { Component } from 'react';\nimport checkIsDepsEqual from './_utils/checkIsDepsEqual';\n\ntype EmptyState = Record<string, never>;\n\n/**\n * Symbol used to indicate that the snapshot should be skipped.\n */\nconst SKIP = Symbol('react-snapshot-bridge-skip');\ntype Skip = typeof SKIP;\n\n/**\n * React logs an error when `getSnapshotBeforeUpdate` returns `undefined`.\n * Boxing the value lets `capture` return `undefined` and still forward it to `apply`.\n */\ntype BoxedSnapshot<T> = {\n readonly value: T;\n};\n\nexport interface SnapshotBeforeUpdateProps<T> {\n /**\n * Runs synchronously **before** the DOM is mutated for this commit.\n * Whatever you return here is forwarded to {@link SnapshotBeforeUpdateProps.apply}.\n * `undefined` is a valid snapshot and is forwarded unchanged.\n */\n capture: () => T;\n\n /**\n * Runs synchronously **after** the DOM is mutated, before the browser paints.\n * Receives the value previously returned from {@link SnapshotBeforeUpdateProps.capture}.\n */\n apply: (snapshot: T) => void;\n\n /**\n * When `false`, both `capture` and `apply` are skipped for that commit.\n * Evaluated against the **current** props at commit time, so toggling\n * `true` -> `false` skips both callbacks for that update, and `false` -> `true`\n * runs both as usual on the next update. Defaults to `true`.\n *\n * Updates that occur while disabled are not buffered or replayed later.\n */\n enabled?: boolean;\n\n /**\n * Optional dependency array, compared shallowly (`Object.is`) against the\n * previous commit. When provided, both `capture` and `apply` run only on the\n * commit where at least one entry changed; commits with unchanged `deps` are\n * skipped. When omitted, both callbacks run on every update (default).\n */\n deps?: readonly unknown[];\n\n /**\n * Optional children. When provided, this component acts as a wrapper and\n * renders its children. When omitted, it renders nothing and you can place\n * it as a sibling next to the elements you care about.\n */\n children?: ReactNode;\n}\n\n/**\n * Bridges React's class-only `getSnapshotBeforeUpdate` lifecycle into trees\n * built with function components.\n *\n * The component re-renders alongside its parent and uses the `getSnapshotBeforeUpdate`\n * / `componentDidUpdate` pair internally to invoke {@link SnapshotBeforeUpdateProps.capture}\n * before DOM mutation and {@link SnapshotBeforeUpdateProps.apply} immediately after.\n */\nexport class SnapshotBeforeUpdate<T = unknown> extends Component<\n SnapshotBeforeUpdateProps<T>,\n EmptyState,\n BoxedSnapshot<T> | Skip\n> {\n override getSnapshotBeforeUpdate(\n prevProps: Readonly<SnapshotBeforeUpdateProps<T>>,\n ): BoxedSnapshot<T> | Skip {\n if (this.props.enabled === false) return SKIP;\n\n if (this.props.deps !== undefined && checkIsDepsEqual(prevProps.deps, this.props.deps))\n return SKIP;\n\n return { value: this.props.capture() };\n }\n\n override componentDidUpdate(\n _prevProps: Readonly<SnapshotBeforeUpdateProps<T>>,\n _prevState: Readonly<EmptyState>,\n snapshot: BoxedSnapshot<T> | Skip,\n ): void {\n if (snapshot === SKIP) return;\n\n this.props.apply(snapshot.value);\n }\n\n override render(): ReactNode {\n return this.props.children ?? null;\n }\n}\n"]}
|