@fluidframework/react 2.90.0 → 2.92.0
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/CHANGELOG.md +8 -0
- package/api-report/react.alpha.api.md +8 -8
- package/lib/alpha.d.ts +1 -1
- package/lib/beta.d.ts +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/public.d.ts +1 -1
- package/lib/reactSharedTreeView.d.ts +6 -6
- package/lib/reactSharedTreeView.d.ts.map +1 -1
- package/lib/reactSharedTreeView.js +16 -18
- package/lib/reactSharedTreeView.js.map +1 -1
- package/lib/test/reactSharedTreeView.spec.js +3 -3
- package/lib/test/reactSharedTreeView.spec.js.map +1 -1
- package/lib/test/text/textEditor.test.js +89 -676
- package/lib/test/text/textEditor.test.js.map +1 -1
- package/lib/test/useObservation.spec.js +8 -8
- package/lib/test/useObservation.spec.js.map +1 -1
- package/lib/test/useTree.spec.js +15 -15
- package/lib/test/useTree.spec.js.map +1 -1
- package/lib/text/index.d.ts +3 -2
- package/lib/text/index.d.ts.map +1 -1
- package/lib/text/index.js +1 -2
- package/lib/text/index.js.map +1 -1
- package/lib/text/plain/index.d.ts +1 -1
- package/lib/text/plain/index.d.ts.map +1 -1
- package/lib/text/plain/index.js +1 -1
- package/lib/text/plain/index.js.map +1 -1
- package/lib/text/plain/plainTextView.d.ts +6 -3
- package/lib/text/plain/plainTextView.d.ts.map +1 -1
- package/lib/text/plain/plainTextView.js +16 -21
- package/lib/text/plain/plainTextView.js.map +1 -1
- package/lib/text/plain/plainUtils.d.ts +1 -0
- package/lib/text/plain/plainUtils.d.ts.map +1 -1
- package/lib/text/plain/plainUtils.js +1 -0
- package/lib/text/plain/plainUtils.js.map +1 -1
- package/lib/useObservation.js +6 -6
- package/lib/useObservation.js.map +1 -1
- package/lib/useTree.d.ts +7 -7
- package/lib/useTree.d.ts.map +1 -1
- package/lib/useTree.js +6 -6
- package/lib/useTree.js.map +1 -1
- package/package.json +16 -17
- package/react.test-files.tar +0 -0
- package/src/index.ts +1 -9
- package/src/reactSharedTreeView.tsx +11 -13
- package/src/text/index.ts +3 -10
- package/src/text/plain/index.ts +1 -1
- package/src/text/plain/plainTextView.tsx +7 -7
- package/src/text/plain/plainUtils.ts +1 -0
- package/src/useObservation.ts +6 -6
- package/src/useTree.ts +19 -12
- package/tsconfig.json +0 -6
- package/lib/test/mochaHooks.js +0 -13
- package/lib/test/mochaHooks.js.map +0 -1
- package/lib/text/formatted/index.d.ts +0 -6
- package/lib/text/formatted/index.d.ts.map +0 -1
- package/lib/text/formatted/index.js +0 -6
- package/lib/text/formatted/index.js.map +0 -1
- package/lib/text/formatted/quillFormattedView.d.ts +0 -54
- package/lib/text/formatted/quillFormattedView.d.ts.map +0 -1
- package/lib/text/formatted/quillFormattedView.js +0 -426
- package/lib/text/formatted/quillFormattedView.js.map +0 -1
- package/lib/text/plain/quillView.d.ts +0 -22
- package/lib/text/plain/quillView.d.ts.map +0 -1
- package/lib/text/plain/quillView.js +0 -112
- package/lib/text/plain/quillView.js.map +0 -1
- package/src/text/formatted/index.ts +0 -11
- package/src/text/formatted/quillFormattedView.tsx +0 -509
- package/src/text/plain/quillView.tsx +0 -149
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Licensed under the MIT License.
|
|
4
|
-
*/
|
|
5
|
-
import * as React from "react";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useRef } from "react";
|
|
6
3
|
import { withMemoizedTreeObservations } from "../../useTree.js";
|
|
7
4
|
import { syncTextToTree } from "./plainUtils.js";
|
|
8
5
|
/**
|
|
@@ -12,7 +9,7 @@ import { syncTextToTree } from "./plainUtils.js";
|
|
|
12
9
|
* @internal
|
|
13
10
|
*/
|
|
14
11
|
export const MainView = ({ root }) => {
|
|
15
|
-
return
|
|
12
|
+
return _jsx(PlainTextEditorView, { root: root });
|
|
16
13
|
};
|
|
17
14
|
/**
|
|
18
15
|
* A plain text editor view component using a native HTML textarea.
|
|
@@ -24,14 +21,14 @@ export const MainView = ({ root }) => {
|
|
|
24
21
|
*/
|
|
25
22
|
const PlainTextEditorView = withMemoizedTreeObservations(({ root }) => {
|
|
26
23
|
// Reference to the textarea element
|
|
27
|
-
const textareaRef =
|
|
24
|
+
const textareaRef = useRef(null);
|
|
28
25
|
// Guards against update loops between textarea and the tree
|
|
29
|
-
const isUpdatingRef =
|
|
26
|
+
const isUpdatingRef = useRef(false);
|
|
30
27
|
// Access tree content during render to establish observation.
|
|
31
28
|
// The HOC will automatically re-render when this content changes.
|
|
32
29
|
const currentText = root.fullString();
|
|
33
30
|
// Handle textarea changes - sync textarea → tree
|
|
34
|
-
const handleChange =
|
|
31
|
+
const handleChange = useCallback((event) => {
|
|
35
32
|
if (isUpdatingRef.current) {
|
|
36
33
|
return;
|
|
37
34
|
}
|
|
@@ -59,17 +56,15 @@ const PlainTextEditorView = withMemoizedTreeObservations(({ root }) => {
|
|
|
59
56
|
isUpdatingRef.current = false;
|
|
60
57
|
}
|
|
61
58
|
}
|
|
62
|
-
return (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
resize: "vertical",
|
|
73
|
-
} })));
|
|
59
|
+
return (_jsxs("div", { className: "text-editor-container", style: { height: "100%", display: "flex", flexDirection: "column" }, children: [_jsx("h2", { style: { margin: "10px 0" }, children: "Collaborative Text Editor" }), _jsx("textarea", { ref: textareaRef, defaultValue: currentText, onChange: handleChange, placeholder: "Start typing...", style: {
|
|
60
|
+
flex: 1,
|
|
61
|
+
minHeight: "300px",
|
|
62
|
+
border: "1px solid #ccc",
|
|
63
|
+
borderRadius: "4px",
|
|
64
|
+
padding: "8px",
|
|
65
|
+
fontSize: "14px",
|
|
66
|
+
fontFamily: "inherit",
|
|
67
|
+
resize: "vertical",
|
|
68
|
+
} })] }));
|
|
74
69
|
});
|
|
75
70
|
//# sourceMappingURL=plainTextView.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plainTextView.js","sourceRoot":"","sources":["../../../src/text/plain/plainTextView.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"plainTextView.js","sourceRoot":"","sources":["../../../src/text/plain/plainTextView.tsx"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport type { TextAsTree } from \"@fluidframework/tree/internal\";\nimport { type ChangeEvent, type FC, useCallback, useRef } from \"react\";\n\nimport type { PropTreeNode } from \"../../propNode.js\";\nimport { withMemoizedTreeObservations } from \"../../useTree.js\";\n\nimport { syncTextToTree } from \"./plainUtils.js\";\n\n/**\n * A React component for plain text editing.\n * @remarks\n * Uses {@link @fluidframework/tree#TextAsTree.Tree} for the data-model and an HTML textarea for the UI.\n * @internal\n */\nexport const MainView: FC<{ root: PropTreeNode<TextAsTree.Tree> }> = ({ root }) => {\n\treturn <PlainTextEditorView root={root} />;\n};\n\n/**\n * A plain text editor view component using a native HTML textarea.\n * Uses TextAsTree for collaborative plain text storage.\n *\n * @remarks\n * This uses withMemoizedTreeObservations to automatically re-render\n * when the tree changes.\n */\nconst PlainTextEditorView = withMemoizedTreeObservations(\n\t({ root }: { root: TextAsTree.Tree }) => {\n\t\t// Reference to the textarea element\n\t\tconst textareaRef = useRef<HTMLTextAreaElement>(null);\n\t\t// Guards against update loops between textarea and the tree\n\t\tconst isUpdatingRef = useRef<boolean>(false);\n\n\t\t// Access tree content during render to establish observation.\n\t\t// The HOC will automatically re-render when this content changes.\n\t\tconst currentText = root.fullString();\n\n\t\t// Handle textarea changes - sync textarea → tree\n\t\tconst handleChange = useCallback(\n\t\t\t(event: ChangeEvent<HTMLTextAreaElement>) => {\n\t\t\t\tif (isUpdatingRef.current) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tisUpdatingRef.current = true;\n\n\t\t\t\tconst newText = event.target.value;\n\t\t\t\tsyncTextToTree(root, newText);\n\n\t\t\t\tisUpdatingRef.current = false;\n\t\t\t},\n\t\t\t[root],\n\t\t);\n\n\t\t// Sync textarea when tree changes externally.\n\t\t// We skip this if isUpdatingRef is true, meaning we caused the tree change ourselves\n\t\t// via the handleChange above - in that case textarea already has the correct content.\n\t\tif (textareaRef.current && !isUpdatingRef.current) {\n\t\t\tconst textareaValue = textareaRef.current.value;\n\n\t\t\t// Only update if content actually differs (avoids cursor jump on local edits)\n\t\t\tif (textareaValue !== currentText) {\n\t\t\t\tisUpdatingRef.current = true;\n\n\t\t\t\t// Preserve cursor position\n\t\t\t\tconst selectionStart = textareaRef.current.selectionStart;\n\t\t\t\tconst selectionEnd = textareaRef.current.selectionEnd;\n\n\t\t\t\ttextareaRef.current.value = currentText;\n\n\t\t\t\t// Restore cursor position, clamped to new text length\n\t\t\t\tconst newPosition = Math.min(selectionStart, currentText.length);\n\t\t\t\tconst newEnd = Math.min(selectionEnd, currentText.length);\n\t\t\t\ttextareaRef.current.setSelectionRange(newPosition, newEnd);\n\n\t\t\t\tisUpdatingRef.current = false;\n\t\t\t}\n\t\t}\n\n\t\treturn (\n\t\t\t<div\n\t\t\t\tclassName=\"text-editor-container\"\n\t\t\t\tstyle={{ height: \"100%\", display: \"flex\", flexDirection: \"column\" }}\n\t\t\t>\n\t\t\t\t<h2 style={{ margin: \"10px 0\" }}>Collaborative Text Editor</h2>\n\t\t\t\t<textarea\n\t\t\t\t\tref={textareaRef}\n\t\t\t\t\tdefaultValue={currentText}\n\t\t\t\t\tonChange={handleChange}\n\t\t\t\t\tplaceholder=\"Start typing...\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tflex: 1,\n\t\t\t\t\t\tminHeight: \"300px\",\n\t\t\t\t\t\tborder: \"1px solid #ccc\",\n\t\t\t\t\t\tborderRadius: \"4px\",\n\t\t\t\t\t\tpadding: \"8px\",\n\t\t\t\t\t\tfontSize: \"14px\",\n\t\t\t\t\t\tfontFamily: \"inherit\",\n\t\t\t\t\t\tresize: \"vertical\",\n\t\t\t\t\t}}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t);\n\t},\n);\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plainUtils.d.ts","sourceRoot":"","sources":["../../../src/text/plain/plainUtils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAEhE
|
|
1
|
+
{"version":3,"file":"plainUtils.d.ts","sourceRoot":"","sources":["../../../src/text/plain/plainUtils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAEhE;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAS3E;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAC5B,QAAQ,EAAE,SAAS,CAAC,EAAE,EACtB,KAAK,EAAE,SAAS,CAAC,EAAE,GACjB;IAAE,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,CAAC,EAAE,CAAA;KAAE,CAAA;CAAE,CAwCxF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plainUtils.js","sourceRoot":"","sources":["../../../src/text/plain/plainUtils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH
|
|
1
|
+
{"version":3,"file":"plainUtils.js","sourceRoot":"","sources":["../../../src/text/plain/plainUtils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,IAAqB,EAAE,OAAe;IACpE,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,cAAc,EAAE,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAE9D,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QACjB,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACtD,CAAC;IACD,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QACjB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;IACjE,CAAC;AACF,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW,CAC1B,QAAsB,EACtB,KAAmB;IAEnB,oDAAoD;IAEpD,IAAI,YAAY,GAAG,CAAC,CAAC;IACrB,OACC,YAAY,GAAG,QAAQ,CAAC,MAAM;QAC9B,YAAY,GAAG,KAAK,CAAC,MAAM;QAC3B,QAAQ,CAAC,YAAY,CAAC,KAAK,KAAK,CAAC,YAAY,CAAC,EAC7C,CAAC;QACF,YAAY,EAAE,CAAC;IAChB,CAAC;IAED,IAAI,YAAY,GAAG,CAAC,CAAC;IACrB,OACC,YAAY,GAAG,YAAY,GAAG,QAAQ,CAAC,MAAM;QAC7C,YAAY,GAAG,YAAY,GAAG,KAAK,CAAC,MAAM;QAC1C,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,GAAG,YAAY,CAAC,KAAK,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,GAAG,YAAY,CAAC,EACtF,CAAC;QACF,YAAY,EAAE,CAAC;IAChB,CAAC;IAED,qDAAqD;IACrD,MAAM,mBAAmB,GAAG,YAAY,CAAC;IACzC,MAAM,iBAAiB,GAAG,QAAQ,CAAC,MAAM,GAAG,YAAY,CAAC;IACzD,MAAM,cAAc,GAAG,YAAY,CAAC;IACpC,MAAM,YAAY,GAAG,KAAK,CAAC,MAAM,GAAG,YAAY,CAAC;IAEjD,OAAO;QACN,MAAM,EACL,mBAAmB,GAAG,iBAAiB;YACtC,CAAC,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,GAAG,EAAE,iBAAiB,EAAE;YACxD,CAAC,CAAC,SAAS;QACb,MAAM,EACL,cAAc,GAAG,YAAY;YAC5B,CAAC,CAAC;gBACA,QAAQ,EAAE,mBAAmB;gBAC7B,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,cAAc,EAAE,YAAY,CAAC;aAChD;YACF,CAAC,CAAC,SAAS;KACb,CAAC;AACH,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport type { TextAsTree } from \"@fluidframework/tree/internal\";\n\n/**\n * Sync `newText` into the provided `root` tree.\n * @internal\n */\nexport function syncTextToTree(root: TextAsTree.Tree, newText: string): void {\n\tconst sync = computeSync(root.charactersCopy(), [...newText]);\n\n\tif (sync.remove) {\n\t\troot.removeRange(sync.remove.start, sync.remove.end);\n\t}\n\tif (sync.insert) {\n\t\troot.insertAt(sync.insert.location, sync.insert.slice.join(\"\"));\n\t}\n}\n\n/**\n * Sync `newText` into the provided `root` tree.\n */\nexport function computeSync<T>(\n\texisting: readonly T[],\n\tfinal: readonly T[],\n): { remove?: { start: number; end: number }; insert?: { location: number; slice: T[] } } {\n\t// Find common prefix and suffix to minimize changes\n\n\tlet prefixLength = 0;\n\twhile (\n\t\tprefixLength < existing.length &&\n\t\tprefixLength < final.length &&\n\t\texisting[prefixLength] === final[prefixLength]\n\t) {\n\t\tprefixLength++;\n\t}\n\n\tlet suffixLength = 0;\n\twhile (\n\t\tsuffixLength + prefixLength < existing.length &&\n\t\tsuffixLength + prefixLength < final.length &&\n\t\texisting[existing.length - 1 - suffixLength] === final[final.length - 1 - suffixLength]\n\t) {\n\t\tsuffixLength++;\n\t}\n\n\t// Locate middle replaced range in existing and final\n\tconst existingMiddleStart = prefixLength;\n\tconst existingMiddleEnd = existing.length - suffixLength;\n\tconst newMiddleStart = prefixLength;\n\tconst newMiddleEnd = final.length - suffixLength;\n\n\treturn {\n\t\tremove:\n\t\t\texistingMiddleStart < existingMiddleEnd\n\t\t\t\t? { start: existingMiddleStart, end: existingMiddleEnd }\n\t\t\t\t: undefined,\n\t\tinsert:\n\t\t\tnewMiddleStart < newMiddleEnd\n\t\t\t\t? {\n\t\t\t\t\t\tlocation: existingMiddleStart,\n\t\t\t\t\t\tslice: final.slice(newMiddleStart, newMiddleEnd),\n\t\t\t\t\t}\n\t\t\t\t: undefined,\n\t};\n}\n"]}
|
package/lib/useObservation.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
|
-
import
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
6
|
/**
|
|
7
7
|
* Wrapper around subscriptions to give it an object identity which can be used with FinalizationRegistry.
|
|
8
8
|
* @remarks
|
|
@@ -38,7 +38,7 @@ class SubscriptionsWrapper {
|
|
|
38
38
|
*/
|
|
39
39
|
export function useObservation(trackDuring, options) {
|
|
40
40
|
// Use a React state hook to invalidate this component something tracked by `trackDuring` changes.
|
|
41
|
-
const [subscriptions, setSubscriptions] =
|
|
41
|
+
const [subscriptions, setSubscriptions] = useState(new SubscriptionsWrapper());
|
|
42
42
|
// Because `subscriptions` is used in `finalizationRegistry` for cleanup, it is important that nothing save a reference to it which is retained by the invalidation callback.
|
|
43
43
|
// TO help with this, pull out `inner` so it can be closed over without retaining `subscriptions`.
|
|
44
44
|
const inner = subscriptions.inner;
|
|
@@ -75,7 +75,7 @@ export function useObservation(trackDuring, options) {
|
|
|
75
75
|
// Suppressing that invalidation bug with an extra call to setSubscriptions could work, but would produce incorrect warnings about leaks,
|
|
76
76
|
// and might cause infinite rerender depending on how StrictMode works.
|
|
77
77
|
// Such an Effect would look like this:
|
|
78
|
-
//
|
|
78
|
+
// useEffect(
|
|
79
79
|
// () => () => {
|
|
80
80
|
// subscriptions.unsubscribe?.();
|
|
81
81
|
// subscriptions.unsubscribe = undefined;
|
|
@@ -116,9 +116,9 @@ const finalizationRegistry = new FinalizationRegistry((subscriptions) => {
|
|
|
116
116
|
*/
|
|
117
117
|
function useObservationPure(trackDuring, options) {
|
|
118
118
|
// Dummy state used to trigger invalidations.
|
|
119
|
-
const [_subscriptions, setSubscriptions] =
|
|
119
|
+
const [_subscriptions, setSubscriptions] = useState(0);
|
|
120
120
|
const { result, subscribe } = trackDuring();
|
|
121
|
-
|
|
121
|
+
useEffect(() => {
|
|
122
122
|
// Subscribe to events from the latest render
|
|
123
123
|
const invalidate = () => {
|
|
124
124
|
setSubscriptions((n) => n + 1);
|
|
@@ -257,7 +257,7 @@ function observationAdapter(trackDuring, options) {
|
|
|
257
257
|
export function useObservationStrict(trackDuring, options) {
|
|
258
258
|
// Used to unsubscribe from the previous render's subscriptions.
|
|
259
259
|
// See `useObservation` for a more documented explanation of this pattern.
|
|
260
|
-
const [subscriptions] =
|
|
260
|
+
const [subscriptions] = useState({ previousTracker: undefined });
|
|
261
261
|
const pureResult = useObservationPure(observationAdapter(trackDuring, options), options);
|
|
262
262
|
subscriptions.previousTracker?.dispose();
|
|
263
263
|
subscriptions.previousTracker = pureResult.tracker;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useObservation.js","sourceRoot":"","sources":["../src/useObservation.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport * as React from \"react\";\n\n/**\n * Tracks and subscriptions from the latests render of a given instance of the {@link useObservation} hook.\n */\ninterface Subscriptions {\n\t/**\n\t * If defined, still needs to be called at some point.\n\t * @remarks\n\t * Clear when called.\n\t */\n\tunsubscribe?: () => void;\n}\n\n/**\n * Wrapper around subscriptions to give it an object identity which can be used with FinalizationRegistry.\n * @remarks\n * This indirection is needed so inner can be provided to finalizationRegistry as the heldValue and avoid having that cause a leak.\n * @privateRemarks\n * This is a named class to make looking for leaks of it in heap snapshots easier.\n */\nclass SubscriptionsWrapper {\n\tpublic readonly inner: Subscriptions = {};\n}\n\n/**\n * Options for {@link useTreeObservations}.\n * @input\n * @alpha\n */\nexport interface ObservationOptions {\n\t/**\n\t * Called when the tracked observations are invalidated.\n\t * @remarks\n\t * This is not expected to have production use cases, but is useful for testing and debugging.\n\t */\n\tonInvalidation?: () => void;\n}\n\n/**\n * Custom hook which invalidates a React Component based on changes to what was observed during `trackDuring`.\n *\n * @param trackDuring - Called synchronously: can make event subscriptions which call the provided `invalidate` function.\n * Any such subscriptions should be cleaned up via the returned `unsubscribe` function which will only be invoked if `invalidate` is not called.\n * If `invalidate` is called, the code calling it should remove any subscriptions before calling it.\n * @remarks\n * React strongly discourages \"render\" from having side-effects other than idempotent lazy initialization.\n *\n * Tracking observations made during render to subscribe to events for automatic invalidation is a side-effect.\n * This makes the behavior of this hook somewhat unusual from a React perspective, and also rather poorly supported by React.\n *\n * That said, the alternatives more aligned with how React expects things to work have much less friendly APIs, or have gaps where they risk invalidation bugs.\n *\n * For example, this hook could record which observations were made during render, then pass them into a `useEffect` hook to do the subscription.\n * This would be more aligned with React's expectations, but would have a number of issues:\n * - The effect would run after render, so if the observed content changed between render and the effect running, there could be an invalidation bug.\n * - It would require changes to `TreeAlpha.trackObservationsOnce` to support a two phase approach (first track, then subscribe) which would have the same risk of missed invalidation.\n * - It would have slightly higher cost due to the extra effect.\n * Such an approach is implemented in {@link useObservationPure}.\n */\nexport function useObservation<TResult>(\n\ttrackDuring: (invalidate: () => void) => { result: TResult; unsubscribe: () => void },\n\toptions?: ObservationOptions,\n): TResult {\n\t// Use a React state hook to invalidate this component something tracked by `trackDuring` changes.\n\tconst [subscriptions, setSubscriptions] = React.useState<SubscriptionsWrapper>(\n\t\tnew SubscriptionsWrapper(),\n\t);\n\n\t// Because `subscriptions` is used in `finalizationRegistry` for cleanup, it is important that nothing save a reference to it which is retained by the invalidation callback.\n\t// TO help with this, pull out `inner` so it can be closed over without retaining `subscriptions`.\n\tconst inner = subscriptions.inner;\n\n\tconst invalidate = (): void => {\n\t\t// Since below uses trackObservationsOnce, the un-subscription is done before calling this callback,\n\t\t// and therefore this must ensure that no further un-subscriptions occur, as well as that the render is invalidated.\n\t\t//\n\t\t// Note referencing `setSubscriptions` risks transitively holding onto a reference to `subscriptions` depending on how React implements `useState`.\n\t\t// If such a transitive reference does exist, it would cause a leak (by preventing finalizationRegistry from running and thus preventing un-subscription after unmount).\n\t\t// Experimentally this has been observed not to be the case, and is validated by the \"unsubscribe on unmount\" tests.\n\t\tsetSubscriptions(new SubscriptionsWrapper());\n\n\t\t// This cannot do `registry.unregister(subscriptions);` as that would cause a leak by holding onto `subscriptions`\n\t\t// since this closure is held onto by the subscribed events.\n\t\t// Skipping such an un-registration is fine so long as we ensure the registry does not redundantly unsubscribe.\n\t\t// Since trackObservationsOnce already unsubscribed, just clear out the unsubscribe function to ensure it is not called again by the finalizer.\n\t\tinner.unsubscribe = undefined;\n\n\t\toptions?.onInvalidation?.();\n\t};\n\n\t// If there was a previous rendering of this instance of this hook in the current component, unsubscribe from it.\n\t// This avoids a memory leak (of the event subscriptions) in the case where a components is rerendered.\n\tinner.unsubscribe?.();\n\tinner.unsubscribe = undefined;\n\n\t// This is logically pure other than the side effect of registering for invalidation if the observed content changes.\n\t// This is safe from a React perspective since when the observed content changes, that is reflected in the `useState` above.\n\t// What is more problematic is avoiding of leaking the event registrations since React does not provide an easy way to do that for code run outside of a hook.\n\t// That leak is avoided via two separate approaches: the un-subscription for events from previous renders above,\n\t// and the use of finalizationRegistry below to handle the component unmount case.\n\tconst out = trackDuring(invalidate);\n\n\tinner.unsubscribe = out.unsubscribe;\n\n\t// There is still the issue of unsubscribing when the component unmounts.\n\t// This can almost be done using a React effect hook with an empty dependency list.\n\t// Unfortunately that would have a hard time getting the correct subscriptions to unsubscribe,\n\t// and if run before unmount, like in StrictMode, it would cause an invalidation bug.\n\t// Suppressing that invalidation bug with an extra call to setSubscriptions could work, but would produce incorrect warnings about leaks,\n\t// and might cause infinite rerender depending on how StrictMode works.\n\t// Such an Effect would look like this:\n\t// React.useEffect(\n\t// \t() => () => {\n\t// \t\tsubscriptions.unsubscribe?.();\n\t// \t\tsubscriptions.unsubscribe = undefined;\n\t// \t\tsetSubscriptions({});\n\t// \t},\n\t// \t[],\n\t// );\n\t// Instead of that, use a FinalizationRegistry to clean up when the subscriptions.\n\t// As this only needs to run sometime after the component is unmounted, triggering it based on React no longer holding onto the subscriptions state object is sufficient.\n\t// This should be safe (not unsubscribe too early) as React will hold onto the state object for as long as the component is mounted since if the component rerenders, it will be required.\n\t// If React decided it would never reuse the component instance (recreate it instead of rerender) but kept it mounted, then it would be possible for this to unsubscribe too early.\n\t// Currently however, it does not seem like React does or will do that.\n\t// If such an issue does ever occur, it could be fixed by stuffing a reference to the `subscriptions` object in the DOM: for now such a mitigation appears unnecessary and would add overhead.\n\tfinalizationRegistry.register(subscriptions, inner);\n\n\treturn out.result;\n}\n\n/**\n * Handles unsubscribing from events when the {@link SubscriptionsWrapper} is garbage collected.\n * See comments in {@link useTreeObservations} for details.\n */\nconst finalizationRegistry = new FinalizationRegistry((subscriptions: Subscriptions) => {\n\tsubscriptions.unsubscribe?.();\n\t// Clear out the unsubscribe function to ensure it is not called again.\n\t// This should not be needed, but maintains the invariant that unsubscribe should be removed after being called.\n\tsubscriptions.unsubscribe = undefined;\n});\n\n//\n// Below here are some alternative approaches.\n// Should issues arise with the above, one of these could be used instead.\n// These alternatives have user facing downsides (mainly performance and/or gaps where they could miss invalidations)\n// so are not being used as long as the above setup seems to be working well enough.\n//\n\n/**\n * Options for {@link useTreeObservations}.\n * @input\n */\nexport interface ObservationPureOptions {\n\tonSubscribe?: () => void;\n\tonUnsubscribe?: () => void;\n\tonPureInvalidation?: () => void;\n}\n\n/**\n * Variant of {@link useObservation} where render behaves in a more pure functional way.\n * @remarks\n * Subscriptions are only created in effects, which leaves a gap between when the observations are tracked and the subscriptions are created.\n * @privateRemarks\n * If impureness of the other approaches becomes a problem, this could be used directly instead.\n * Doing so would require changing `TreeAlpha.trackObservationsOnce` return a function to subscribe to the tracked observations instead of subscribing directly.\n * This would be less robust (edits could be missed between render and the effect running) but would avoid the impure aspects of the other approaches.\n * This would remove the need for a finalizationRegistry, and would avoid relying on React not doing something unexpected like rendering a component twice and throwing away the second render instead of the first.\n *\n * If using this directly, ensure it has tests other than via the other hooks which use it.\n */\nfunction useObservationPure<TResult>(\n\ttrackDuring: () => { result: TResult; subscribe: (invalidate: () => void) => () => void },\n\toptions?: ObservationPureOptions,\n): TResult {\n\t// Dummy state used to trigger invalidations.\n\tconst [_subscriptions, setSubscriptions] = React.useState(0);\n\n\tconst { result, subscribe } = trackDuring();\n\n\tReact.useEffect(() => {\n\t\t// Subscribe to events from the latest render\n\n\t\tconst invalidate = (): void => {\n\t\t\tsetSubscriptions((n) => n + 1);\n\t\t\tinner.unsubscribe = undefined;\n\t\t\toptions?.onPureInvalidation?.();\n\t\t};\n\n\t\toptions?.onSubscribe?.();\n\t\tconst inner: Subscriptions = { unsubscribe: subscribe(invalidate) };\n\n\t\treturn () => {\n\t\t\tinner.unsubscribe?.();\n\t\t\tinner.unsubscribe = undefined;\n\t\t\toptions?.onUnsubscribe?.();\n\t\t};\n\t});\n\treturn result;\n}\n\n/**\n * Manages subscription to a one-shot invalidation event (unsubscribes when sent) event where multiple parties may want to subscribe to the event.\n * @remarks\n * When the event occurs, all subscribers are called.\n * Any subscribers added after the event has occurred are immediately called.\n *\n * Since new subscriptions can be added any any time, this can not unsubscribe from the source after the last destination has unsubscribed.\n *\n * Instead the finalizationRegistry is used.\n * @privateRemarks\n * This is a named class to make looking for leaks of it in heap snapshots easier.\n */\nclass SubscriptionTracker {\n\t/**\n\t * Subscriptions to underlying events.\n\t */\n\tprivate readonly inner: Subscriptions;\n\t/**\n\t * Hook subscriptions to be trigger by `inner`.\n\t */\n\tprivate readonly toInvalidate = new Set<() => void>();\n\n\tprivate disposed: boolean = false;\n\n\tprivate constructor(unsubscribe: () => void) {\n\t\tthis.inner = { unsubscribe };\n\t}\n\n\tprivate assertNotDisposed(): void {\n\t\tif (this.disposed) {\n\t\t\tthrow new Error(\"Already disposed\");\n\t\t}\n\t}\n\n\tpublic readonly invalidate = (): void => {\n\t\tthis.assertNotDisposed();\n\t\tif (this.inner.unsubscribe === undefined) {\n\t\t\tthrow new Error(\"Already invalidated\");\n\t\t}\n\n\t\tthis.inner.unsubscribe = undefined;\n\n\t\tfor (const invalidate of this.toInvalidate) {\n\t\t\tinvalidate();\n\t\t}\n\t\tthis.toInvalidate.clear();\n\t};\n\n\tpublic static create(unsubscribe: () => void): SubscriptionTracker {\n\t\tconst tracker = new SubscriptionTracker(unsubscribe);\n\t\tfinalizationRegistry.register(tracker, tracker.inner);\n\t\treturn tracker;\n\t}\n\n\tpublic subscribe(callback: () => void): () => void {\n\t\tthis.assertNotDisposed();\n\t\tif (this.toInvalidate.has(callback)) {\n\t\t\tthrow new Error(\"Already subscribed\");\n\t\t}\n\n\t\tif (this.inner.unsubscribe === undefined) {\n\t\t\t// Already invalidated, so immediately call back.\n\t\t\tcallback();\n\t\t\treturn () => {};\n\t\t}\n\n\t\tthis.toInvalidate.add(callback);\n\n\t\treturn () => {\n\t\t\tthis.assertNotDisposed();\n\t\t\tif (!this.toInvalidate.has(callback)) {\n\t\t\t\tthrow new Error(\"Not subscribed\");\n\t\t\t}\n\t\t\tthis.toInvalidate.delete(callback);\n\t\t};\n\t}\n\n\tpublic dispose(): void {\n\t\tthis.assertNotDisposed();\n\t\tthis.disposed = true;\n\t\tthis.inner.unsubscribe?.();\n\t\tthis.inner.unsubscribe = undefined;\n\n\t\tif (this.toInvalidate.size > 0) {\n\t\t\tthrow new Error(\"Invalid disposal before unsubscribing all listeners\");\n\t\t}\n\n\t\tfinalizationRegistry.unregister(this.inner);\n\t}\n}\n\n/**\n * {@link useObservation} but more aligned with React expectations.\n * @remarks\n * This is more expensive than {@link useObservation}, and also leaks subscriptions longer.\n * When rendering a component, relies on a finalizer to clean up subscriptions from the previous render.\n *\n * Unlike {@link useObservation}, this behave correctly even if React does something unexpected, like Rendering a component twice, and throwing away the second render instead of the first.\n * {@link useObservation} relies on React not doing such things, assuming that when re-rendering a component, it will be the older render which is discarded.\n *\n * This should also avoid calling `setState` after unmount, which can avoid a React warning.\n *\n * This does not however avoid the finalizer based cleanup: it actually relies on it much more (for rerender and unmount, not just unmount).\n * This simply adds a layer of indirection to the invalidation through useEffect.\n */\nexport function useObservationWithEffects<TResult>(\n\ttrackDuring: (invalidate: () => void) => { result: TResult; unsubscribe: () => void },\n\toptions?: ObservationOptions & ObservationPureOptions,\n): TResult {\n\tconst pureResult = useObservationPure(observationAdapter(trackDuring, options), options);\n\treturn pureResult.innerResult;\n}\n\n/**\n * An adapter wrapping `trackDuring` to help implement the {@link useObservation} using {@link useObservationPure}.\n */\nfunction observationAdapter<TResult>(\n\ttrackDuring: (invalidate: () => void) => { result: TResult; unsubscribe: () => void },\n\toptions?: ObservationOptions & ObservationPureOptions,\n): () => {\n\tresult: {\n\t\ttracker: SubscriptionTracker;\n\t\tinnerResult: TResult;\n\t};\n\tsubscribe: (invalidate: () => void) => () => void;\n} {\n\treturn () => {\n\t\t// The main invalidation function, which only runs once, and is used to create the SubscriptionTracker.\n\t\tconst invalidateMain = (): void => {\n\t\t\ttracker.invalidate();\n\t\t\toptions?.onInvalidation?.();\n\t\t};\n\t\tconst result2 = trackDuring(invalidateMain);\n\t\tconst tracker = SubscriptionTracker.create(result2.unsubscribe);\n\n\t\treturn {\n\t\t\tresult: { tracker, innerResult: result2.result },\n\t\t\tsubscribe: (invalidate) => {\n\t\t\t\treturn tracker.subscribe(invalidate);\n\t\t\t},\n\t\t};\n\t};\n}\n\n/**\n * {@link useObservation} but more strict with its behavior.\n * @remarks\n * This has the eager cleanup on re-render of {@link useObservation}, but has the effect based subscriptions and cleanup on unmount of {@link useObservationWithEffects}.\n *\n * If React behaves in a way which breaks the assumptions of {@link useObservation} (and thus would require the leakier {@link useObservationWithEffects}), this will throw an error.\n * @privateRemarks\n * This is just a {@link useObservationPure}, except with the eager cleanup on re-render from {@link useObservation}.\n */\nexport function useObservationStrict<TResult>(\n\ttrackDuring: (invalidate: () => void) => { result: TResult; unsubscribe: () => void },\n\toptions?: ObservationOptions & ObservationPureOptions,\n): TResult {\n\t// Used to unsubscribe from the previous render's subscriptions.\n\t// See `useObservation` for a more documented explanation of this pattern.\n\tconst [subscriptions] = React.useState<{\n\t\tpreviousTracker: SubscriptionTracker | undefined;\n\t}>({ previousTracker: undefined });\n\n\tconst pureResult = useObservationPure(observationAdapter(trackDuring, options), options);\n\n\tsubscriptions.previousTracker?.dispose();\n\tsubscriptions.previousTracker = pureResult.tracker;\n\n\treturn pureResult.innerResult;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"useObservation.js","sourceRoot":"","sources":["../src/useObservation.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { useEffect, useState } from \"react\";\n\n/**\n * Tracks and subscriptions from the latests render of a given instance of the {@link useObservation} hook.\n */\ninterface Subscriptions {\n\t/**\n\t * If defined, still needs to be called at some point.\n\t * @remarks\n\t * Clear when called.\n\t */\n\tunsubscribe?: () => void;\n}\n\n/**\n * Wrapper around subscriptions to give it an object identity which can be used with FinalizationRegistry.\n * @remarks\n * This indirection is needed so inner can be provided to finalizationRegistry as the heldValue and avoid having that cause a leak.\n * @privateRemarks\n * This is a named class to make looking for leaks of it in heap snapshots easier.\n */\nclass SubscriptionsWrapper {\n\tpublic readonly inner: Subscriptions = {};\n}\n\n/**\n * Options for {@link useTreeObservations}.\n * @input\n * @alpha\n */\nexport interface ObservationOptions {\n\t/**\n\t * Called when the tracked observations are invalidated.\n\t * @remarks\n\t * This is not expected to have production use cases, but is useful for testing and debugging.\n\t */\n\tonInvalidation?: () => void;\n}\n\n/**\n * Custom hook which invalidates a React Component based on changes to what was observed during `trackDuring`.\n *\n * @param trackDuring - Called synchronously: can make event subscriptions which call the provided `invalidate` function.\n * Any such subscriptions should be cleaned up via the returned `unsubscribe` function which will only be invoked if `invalidate` is not called.\n * If `invalidate` is called, the code calling it should remove any subscriptions before calling it.\n * @remarks\n * React strongly discourages \"render\" from having side-effects other than idempotent lazy initialization.\n *\n * Tracking observations made during render to subscribe to events for automatic invalidation is a side-effect.\n * This makes the behavior of this hook somewhat unusual from a React perspective, and also rather poorly supported by React.\n *\n * That said, the alternatives more aligned with how React expects things to work have much less friendly APIs, or have gaps where they risk invalidation bugs.\n *\n * For example, this hook could record which observations were made during render, then pass them into a `useEffect` hook to do the subscription.\n * This would be more aligned with React's expectations, but would have a number of issues:\n * - The effect would run after render, so if the observed content changed between render and the effect running, there could be an invalidation bug.\n * - It would require changes to `TreeAlpha.trackObservationsOnce` to support a two phase approach (first track, then subscribe) which would have the same risk of missed invalidation.\n * - It would have slightly higher cost due to the extra effect.\n * Such an approach is implemented in {@link useObservationPure}.\n */\nexport function useObservation<TResult>(\n\ttrackDuring: (invalidate: () => void) => { result: TResult; unsubscribe: () => void },\n\toptions?: ObservationOptions,\n): TResult {\n\t// Use a React state hook to invalidate this component something tracked by `trackDuring` changes.\n\tconst [subscriptions, setSubscriptions] = useState<SubscriptionsWrapper>(\n\t\tnew SubscriptionsWrapper(),\n\t);\n\n\t// Because `subscriptions` is used in `finalizationRegistry` for cleanup, it is important that nothing save a reference to it which is retained by the invalidation callback.\n\t// TO help with this, pull out `inner` so it can be closed over without retaining `subscriptions`.\n\tconst inner = subscriptions.inner;\n\n\tconst invalidate = (): void => {\n\t\t// Since below uses trackObservationsOnce, the un-subscription is done before calling this callback,\n\t\t// and therefore this must ensure that no further un-subscriptions occur, as well as that the render is invalidated.\n\t\t//\n\t\t// Note referencing `setSubscriptions` risks transitively holding onto a reference to `subscriptions` depending on how React implements `useState`.\n\t\t// If such a transitive reference does exist, it would cause a leak (by preventing finalizationRegistry from running and thus preventing un-subscription after unmount).\n\t\t// Experimentally this has been observed not to be the case, and is validated by the \"unsubscribe on unmount\" tests.\n\t\tsetSubscriptions(new SubscriptionsWrapper());\n\n\t\t// This cannot do `registry.unregister(subscriptions);` as that would cause a leak by holding onto `subscriptions`\n\t\t// since this closure is held onto by the subscribed events.\n\t\t// Skipping such an un-registration is fine so long as we ensure the registry does not redundantly unsubscribe.\n\t\t// Since trackObservationsOnce already unsubscribed, just clear out the unsubscribe function to ensure it is not called again by the finalizer.\n\t\tinner.unsubscribe = undefined;\n\n\t\toptions?.onInvalidation?.();\n\t};\n\n\t// If there was a previous rendering of this instance of this hook in the current component, unsubscribe from it.\n\t// This avoids a memory leak (of the event subscriptions) in the case where a components is rerendered.\n\tinner.unsubscribe?.();\n\tinner.unsubscribe = undefined;\n\n\t// This is logically pure other than the side effect of registering for invalidation if the observed content changes.\n\t// This is safe from a React perspective since when the observed content changes, that is reflected in the `useState` above.\n\t// What is more problematic is avoiding of leaking the event registrations since React does not provide an easy way to do that for code run outside of a hook.\n\t// That leak is avoided via two separate approaches: the un-subscription for events from previous renders above,\n\t// and the use of finalizationRegistry below to handle the component unmount case.\n\tconst out = trackDuring(invalidate);\n\n\tinner.unsubscribe = out.unsubscribe;\n\n\t// There is still the issue of unsubscribing when the component unmounts.\n\t// This can almost be done using a React effect hook with an empty dependency list.\n\t// Unfortunately that would have a hard time getting the correct subscriptions to unsubscribe,\n\t// and if run before unmount, like in StrictMode, it would cause an invalidation bug.\n\t// Suppressing that invalidation bug with an extra call to setSubscriptions could work, but would produce incorrect warnings about leaks,\n\t// and might cause infinite rerender depending on how StrictMode works.\n\t// Such an Effect would look like this:\n\t// useEffect(\n\t// \t() => () => {\n\t// \t\tsubscriptions.unsubscribe?.();\n\t// \t\tsubscriptions.unsubscribe = undefined;\n\t// \t\tsetSubscriptions({});\n\t// \t},\n\t// \t[],\n\t// );\n\t// Instead of that, use a FinalizationRegistry to clean up when the subscriptions.\n\t// As this only needs to run sometime after the component is unmounted, triggering it based on React no longer holding onto the subscriptions state object is sufficient.\n\t// This should be safe (not unsubscribe too early) as React will hold onto the state object for as long as the component is mounted since if the component rerenders, it will be required.\n\t// If React decided it would never reuse the component instance (recreate it instead of rerender) but kept it mounted, then it would be possible for this to unsubscribe too early.\n\t// Currently however, it does not seem like React does or will do that.\n\t// If such an issue does ever occur, it could be fixed by stuffing a reference to the `subscriptions` object in the DOM: for now such a mitigation appears unnecessary and would add overhead.\n\tfinalizationRegistry.register(subscriptions, inner);\n\n\treturn out.result;\n}\n\n/**\n * Handles unsubscribing from events when the {@link SubscriptionsWrapper} is garbage collected.\n * See comments in {@link useTreeObservations} for details.\n */\nconst finalizationRegistry = new FinalizationRegistry((subscriptions: Subscriptions) => {\n\tsubscriptions.unsubscribe?.();\n\t// Clear out the unsubscribe function to ensure it is not called again.\n\t// This should not be needed, but maintains the invariant that unsubscribe should be removed after being called.\n\tsubscriptions.unsubscribe = undefined;\n});\n\n//\n// Below here are some alternative approaches.\n// Should issues arise with the above, one of these could be used instead.\n// These alternatives have user facing downsides (mainly performance and/or gaps where they could miss invalidations)\n// so are not being used as long as the above setup seems to be working well enough.\n//\n\n/**\n * Options for {@link useTreeObservations}.\n * @input\n */\nexport interface ObservationPureOptions {\n\tonSubscribe?: () => void;\n\tonUnsubscribe?: () => void;\n\tonPureInvalidation?: () => void;\n}\n\n/**\n * Variant of {@link useObservation} where render behaves in a more pure functional way.\n * @remarks\n * Subscriptions are only created in effects, which leaves a gap between when the observations are tracked and the subscriptions are created.\n * @privateRemarks\n * If impureness of the other approaches becomes a problem, this could be used directly instead.\n * Doing so would require changing `TreeAlpha.trackObservationsOnce` return a function to subscribe to the tracked observations instead of subscribing directly.\n * This would be less robust (edits could be missed between render and the effect running) but would avoid the impure aspects of the other approaches.\n * This would remove the need for a finalizationRegistry, and would avoid relying on React not doing something unexpected like rendering a component twice and throwing away the second render instead of the first.\n *\n * If using this directly, ensure it has tests other than via the other hooks which use it.\n */\nfunction useObservationPure<TResult>(\n\ttrackDuring: () => { result: TResult; subscribe: (invalidate: () => void) => () => void },\n\toptions?: ObservationPureOptions,\n): TResult {\n\t// Dummy state used to trigger invalidations.\n\tconst [_subscriptions, setSubscriptions] = useState(0);\n\n\tconst { result, subscribe } = trackDuring();\n\n\tuseEffect(() => {\n\t\t// Subscribe to events from the latest render\n\n\t\tconst invalidate = (): void => {\n\t\t\tsetSubscriptions((n) => n + 1);\n\t\t\tinner.unsubscribe = undefined;\n\t\t\toptions?.onPureInvalidation?.();\n\t\t};\n\n\t\toptions?.onSubscribe?.();\n\t\tconst inner: Subscriptions = { unsubscribe: subscribe(invalidate) };\n\n\t\treturn () => {\n\t\t\tinner.unsubscribe?.();\n\t\t\tinner.unsubscribe = undefined;\n\t\t\toptions?.onUnsubscribe?.();\n\t\t};\n\t});\n\treturn result;\n}\n\n/**\n * Manages subscription to a one-shot invalidation event (unsubscribes when sent) event where multiple parties may want to subscribe to the event.\n * @remarks\n * When the event occurs, all subscribers are called.\n * Any subscribers added after the event has occurred are immediately called.\n *\n * Since new subscriptions can be added any any time, this can not unsubscribe from the source after the last destination has unsubscribed.\n *\n * Instead the finalizationRegistry is used.\n * @privateRemarks\n * This is a named class to make looking for leaks of it in heap snapshots easier.\n */\nclass SubscriptionTracker {\n\t/**\n\t * Subscriptions to underlying events.\n\t */\n\tprivate readonly inner: Subscriptions;\n\t/**\n\t * Hook subscriptions to be trigger by `inner`.\n\t */\n\tprivate readonly toInvalidate = new Set<() => void>();\n\n\tprivate disposed: boolean = false;\n\n\tprivate constructor(unsubscribe: () => void) {\n\t\tthis.inner = { unsubscribe };\n\t}\n\n\tprivate assertNotDisposed(): void {\n\t\tif (this.disposed) {\n\t\t\tthrow new Error(\"Already disposed\");\n\t\t}\n\t}\n\n\tpublic readonly invalidate = (): void => {\n\t\tthis.assertNotDisposed();\n\t\tif (this.inner.unsubscribe === undefined) {\n\t\t\tthrow new Error(\"Already invalidated\");\n\t\t}\n\n\t\tthis.inner.unsubscribe = undefined;\n\n\t\tfor (const invalidate of this.toInvalidate) {\n\t\t\tinvalidate();\n\t\t}\n\t\tthis.toInvalidate.clear();\n\t};\n\n\tpublic static create(unsubscribe: () => void): SubscriptionTracker {\n\t\tconst tracker = new SubscriptionTracker(unsubscribe);\n\t\tfinalizationRegistry.register(tracker, tracker.inner);\n\t\treturn tracker;\n\t}\n\n\tpublic subscribe(callback: () => void): () => void {\n\t\tthis.assertNotDisposed();\n\t\tif (this.toInvalidate.has(callback)) {\n\t\t\tthrow new Error(\"Already subscribed\");\n\t\t}\n\n\t\tif (this.inner.unsubscribe === undefined) {\n\t\t\t// Already invalidated, so immediately call back.\n\t\t\tcallback();\n\t\t\treturn () => {};\n\t\t}\n\n\t\tthis.toInvalidate.add(callback);\n\n\t\treturn () => {\n\t\t\tthis.assertNotDisposed();\n\t\t\tif (!this.toInvalidate.has(callback)) {\n\t\t\t\tthrow new Error(\"Not subscribed\");\n\t\t\t}\n\t\t\tthis.toInvalidate.delete(callback);\n\t\t};\n\t}\n\n\tpublic dispose(): void {\n\t\tthis.assertNotDisposed();\n\t\tthis.disposed = true;\n\t\tthis.inner.unsubscribe?.();\n\t\tthis.inner.unsubscribe = undefined;\n\n\t\tif (this.toInvalidate.size > 0) {\n\t\t\tthrow new Error(\"Invalid disposal before unsubscribing all listeners\");\n\t\t}\n\n\t\tfinalizationRegistry.unregister(this.inner);\n\t}\n}\n\n/**\n * {@link useObservation} but more aligned with React expectations.\n * @remarks\n * This is more expensive than {@link useObservation}, and also leaks subscriptions longer.\n * When rendering a component, relies on a finalizer to clean up subscriptions from the previous render.\n *\n * Unlike {@link useObservation}, this behave correctly even if React does something unexpected, like Rendering a component twice, and throwing away the second render instead of the first.\n * {@link useObservation} relies on React not doing such things, assuming that when re-rendering a component, it will be the older render which is discarded.\n *\n * This should also avoid calling `setState` after unmount, which can avoid a React warning.\n *\n * This does not however avoid the finalizer based cleanup: it actually relies on it much more (for rerender and unmount, not just unmount).\n * This simply adds a layer of indirection to the invalidation through useEffect.\n */\nexport function useObservationWithEffects<TResult>(\n\ttrackDuring: (invalidate: () => void) => { result: TResult; unsubscribe: () => void },\n\toptions?: ObservationOptions & ObservationPureOptions,\n): TResult {\n\tconst pureResult = useObservationPure(observationAdapter(trackDuring, options), options);\n\treturn pureResult.innerResult;\n}\n\n/**\n * An adapter wrapping `trackDuring` to help implement the {@link useObservation} using {@link useObservationPure}.\n */\nfunction observationAdapter<TResult>(\n\ttrackDuring: (invalidate: () => void) => { result: TResult; unsubscribe: () => void },\n\toptions?: ObservationOptions & ObservationPureOptions,\n): () => {\n\tresult: {\n\t\ttracker: SubscriptionTracker;\n\t\tinnerResult: TResult;\n\t};\n\tsubscribe: (invalidate: () => void) => () => void;\n} {\n\treturn () => {\n\t\t// The main invalidation function, which only runs once, and is used to create the SubscriptionTracker.\n\t\tconst invalidateMain = (): void => {\n\t\t\ttracker.invalidate();\n\t\t\toptions?.onInvalidation?.();\n\t\t};\n\t\tconst result2 = trackDuring(invalidateMain);\n\t\tconst tracker = SubscriptionTracker.create(result2.unsubscribe);\n\n\t\treturn {\n\t\t\tresult: { tracker, innerResult: result2.result },\n\t\t\tsubscribe: (invalidate) => {\n\t\t\t\treturn tracker.subscribe(invalidate);\n\t\t\t},\n\t\t};\n\t};\n}\n\n/**\n * {@link useObservation} but more strict with its behavior.\n * @remarks\n * This has the eager cleanup on re-render of {@link useObservation}, but has the effect based subscriptions and cleanup on unmount of {@link useObservationWithEffects}.\n *\n * If React behaves in a way which breaks the assumptions of {@link useObservation} (and thus would require the leakier {@link useObservationWithEffects}), this will throw an error.\n * @privateRemarks\n * This is just a {@link useObservationPure}, except with the eager cleanup on re-render from {@link useObservation}.\n */\nexport function useObservationStrict<TResult>(\n\ttrackDuring: (invalidate: () => void) => { result: TResult; unsubscribe: () => void },\n\toptions?: ObservationOptions & ObservationPureOptions,\n): TResult {\n\t// Used to unsubscribe from the previous render's subscriptions.\n\t// See `useObservation` for a more documented explanation of this pattern.\n\tconst [subscriptions] = useState<{\n\t\tpreviousTracker: SubscriptionTracker | undefined;\n\t}>({ previousTracker: undefined });\n\n\tconst pureResult = useObservationPure(observationAdapter(trackDuring, options), options);\n\n\tsubscriptions.previousTracker?.dispose();\n\tsubscriptions.previousTracker = pureResult.tracker;\n\n\treturn pureResult.innerResult;\n}\n"]}
|
package/lib/useTree.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
import type { TreeLeafValue, TreeNode } from "@fluidframework/tree";
|
|
6
|
-
import
|
|
6
|
+
import { type FC, memo, type MemoExoticComponent } from "react";
|
|
7
7
|
import { type PropTreeNodeRecord, type PropTreeValue, type UnwrapPropTreeNodeRecord, type WrapNodes } from "./propNode.js";
|
|
8
8
|
import { type ObservationOptions } from "./useObservation.js";
|
|
9
9
|
/**
|
|
@@ -25,20 +25,20 @@ export declare function useTree(subtreeRoot: TreeNode): number;
|
|
|
25
25
|
* It is recommended that sub-components which take in TreeNodes, if not defined using this higher order components, take the nodes in as {@link PropTreeNode}s.
|
|
26
26
|
* Components defined using this higher order component can take in either raw TreeNodes or {@link PropTreeNode}s: the latter will be automatically unwrapped.
|
|
27
27
|
* @privateRemarks
|
|
28
|
-
* `
|
|
28
|
+
* `FC` does not seem to be covariant over its input type, so to make use of this more ergonomic,
|
|
29
29
|
* the return type intersects the various ways this could be used (with or without PropTreeNode wrapping).
|
|
30
30
|
* @alpha
|
|
31
31
|
*/
|
|
32
|
-
export declare function withTreeObservations<TIn>(component:
|
|
32
|
+
export declare function withTreeObservations<TIn>(component: FC<TIn>, options?: ObservationOptions): FC<TIn> & FC<WrapNodes<TIn>> & FC<TIn | WrapNodes<TIn>>;
|
|
33
33
|
/**
|
|
34
|
-
* {@link withTreeObservations} wrapped with
|
|
34
|
+
* {@link withTreeObservations} wrapped with memo.
|
|
35
35
|
* @remarks
|
|
36
36
|
* There is no special logic here, just a convenience wrapper.
|
|
37
37
|
* @alpha
|
|
38
38
|
*/
|
|
39
|
-
export declare function withMemoizedTreeObservations<TIn>(component:
|
|
40
|
-
readonly propsAreEqual?: Parameters<typeof
|
|
41
|
-
}):
|
|
39
|
+
export declare function withMemoizedTreeObservations<TIn>(component: FC<TIn>, options?: ObservationOptions & {
|
|
40
|
+
readonly propsAreEqual?: Parameters<typeof memo>[1];
|
|
41
|
+
}): MemoExoticComponent<ReturnType<typeof withTreeObservations<TIn>>>;
|
|
42
42
|
/**
|
|
43
43
|
* Custom hook which invalidates a React Component when there is a change in tree content observed during `trackDuring`.
|
|
44
44
|
*
|
package/lib/useTree.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useTree.d.ts","sourceRoot":"","sources":["../src/useTree.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAGpE,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"useTree.d.ts","sourceRoot":"","sources":["../src/useTree.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAGpE,OAAO,EACN,KAAK,EAAE,EACP,IAAI,EACJ,KAAK,mBAAmB,EAIxB,MAAM,OAAO,CAAC;AAEf,OAAO,EAGN,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,KAAK,wBAAwB,EAC7B,KAAK,SAAS,EACd,MAAM,eAAe,CAAC;AACvB,OAAO,EAAkB,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAE9E;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,WAAW,EAAE,QAAQ,GAAG,MAAM,CAcrD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EACvC,SAAS,EAAE,EAAE,CAAC,GAAG,CAAC,EAClB,OAAO,CAAC,EAAE,kBAAkB,GAC1B,EAAE,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAGzD;AAED;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,GAAG,EAC/C,SAAS,EAAE,EAAE,CAAC,GAAG,CAAC,EAClB,OAAO,CAAC,EAAE,kBAAkB,GAAG;IAC9B,QAAQ,CAAC,aAAa,CAAC,EAAE,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;CACpD,GACC,mBAAmB,CAAC,UAAU,CAAC,OAAO,oBAAoB,CAAC,GAAG,CAAC,CAAC,CAAC,CAEnE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAC1C,WAAW,EAAE,MAAM,OAAO,EAC1B,OAAO,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAKT;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,QAAQ,GAAG,aAAa,EAAE,OAAO,EAC1E,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,EAC9B,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,GAC/B,SAAS,CAAC,OAAO,CAAC,CAMpB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,CAAC,CAAC,SAAS,kBAAkB,EAAE,OAAO,EAC5E,KAAK,EAAE,CAAC,EACR,CAAC,EAAE,CAAC,IAAI,EAAE,wBAAwB,CAAC,CAAC,CAAC,KAAK,OAAO,GAC/C,SAAS,CAAC,OAAO,CAAC,CAMpB"}
|
package/lib/useTree.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Tree } from "@fluidframework/tree";
|
|
6
6
|
import { TreeAlpha } from "@fluidframework/tree/internal";
|
|
7
|
-
import
|
|
7
|
+
import { memo, useEffect, useState, } from "react";
|
|
8
8
|
import { unwrapPropTreeNode, unwrapPropTreeRecord, } from "./propNode.js";
|
|
9
9
|
import { useObservation } from "./useObservation.js";
|
|
10
10
|
/**
|
|
@@ -18,9 +18,9 @@ import { useObservation } from "./useObservation.js";
|
|
|
18
18
|
export function useTree(subtreeRoot) {
|
|
19
19
|
// Use a React effect hook to invalidate this component when the subtreeRoot changes.
|
|
20
20
|
// We do this by incrementing a counter, which is passed as a dependency to the effect hook.
|
|
21
|
-
const [invalidations, setInvalidations] =
|
|
21
|
+
const [invalidations, setInvalidations] = useState(0);
|
|
22
22
|
// React effect hook that increments the 'invalidation' counter whenever subtreeRoot or any of its children change.
|
|
23
|
-
|
|
23
|
+
useEffect(() => {
|
|
24
24
|
// Returns the cleanup function to be invoked when the component unmounts.
|
|
25
25
|
return Tree.on(subtreeRoot, "treeChanged", () => {
|
|
26
26
|
setInvalidations((i) => i + 1);
|
|
@@ -38,7 +38,7 @@ export function useTree(subtreeRoot) {
|
|
|
38
38
|
* It is recommended that sub-components which take in TreeNodes, if not defined using this higher order components, take the nodes in as {@link PropTreeNode}s.
|
|
39
39
|
* Components defined using this higher order component can take in either raw TreeNodes or {@link PropTreeNode}s: the latter will be automatically unwrapped.
|
|
40
40
|
* @privateRemarks
|
|
41
|
-
* `
|
|
41
|
+
* `FC` does not seem to be covariant over its input type, so to make use of this more ergonomic,
|
|
42
42
|
* the return type intersects the various ways this could be used (with or without PropTreeNode wrapping).
|
|
43
43
|
* @alpha
|
|
44
44
|
*/
|
|
@@ -46,13 +46,13 @@ export function withTreeObservations(component, options) {
|
|
|
46
46
|
return (props) => useTreeObservations(() => component(props), options);
|
|
47
47
|
}
|
|
48
48
|
/**
|
|
49
|
-
* {@link withTreeObservations} wrapped with
|
|
49
|
+
* {@link withTreeObservations} wrapped with memo.
|
|
50
50
|
* @remarks
|
|
51
51
|
* There is no special logic here, just a convenience wrapper.
|
|
52
52
|
* @alpha
|
|
53
53
|
*/
|
|
54
54
|
export function withMemoizedTreeObservations(component, options) {
|
|
55
|
-
return
|
|
55
|
+
return memo(withTreeObservations(component, options), options?.propsAreEqual);
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
58
|
* Custom hook which invalidates a React Component when there is a change in tree content observed during `trackDuring`.
|
package/lib/useTree.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useTree.js","sourceRoot":"","sources":["../src/useTree.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAC1D,OAAO,
|
|
1
|
+
{"version":3,"file":"useTree.js","sourceRoot":"","sources":["../src/useTree.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAC1D,OAAO,EAEN,IAAI,EAGJ,SAAS,EACT,QAAQ,GACR,MAAM,OAAO,CAAC;AAEf,OAAO,EACN,kBAAkB,EAClB,oBAAoB,GAKpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,cAAc,EAA2B,MAAM,qBAAqB,CAAC;AAE9E;;;;;;;GAOG;AACH,MAAM,UAAU,OAAO,CAAC,WAAqB;IAC5C,qFAAqF;IACrF,4FAA4F;IAC5F,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAEtD,mHAAmH;IACnH,SAAS,CAAC,GAAG,EAAE;QACd,0EAA0E;QAC1E,OAAO,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE,aAAa,EAAE,GAAG,EAAE;YAC/C,gBAAgB,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAChC,CAAC,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC,CAAC;IAEjC,OAAO,aAAa,CAAC;AACtB,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,oBAAoB,CACnC,SAAkB,EAClB,OAA4B;IAE5B,OAAO,CAAC,KAA2B,EAAa,EAAE,CACjD,mBAAmB,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,KAAY,CAAC,EAAE,OAAO,CAAC,CAAC;AAC9D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,4BAA4B,CAC3C,SAAkB,EAClB,OAEC;IAED,OAAO,IAAI,CAAC,oBAAoB,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC;AAC/E,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,mBAAmB,CAClC,WAA0B,EAC1B,OAA4B;IAE5B,OAAO,cAAc,CACpB,CAAC,UAAU,EAAE,EAAE,CAAC,SAAS,CAAC,qBAAqB,CAAC,UAAU,EAAE,WAAW,CAAC,EACxE,OAAO,CACP,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,eAAe,CAC9B,QAA8B,EAC9B,WAAiC;IAEjC,MAAM,IAAI,GAAM,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAE7C,MAAM,MAAM,GAAG,mBAAmB,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC;IAE5D,OAAO,MAA4B,CAAC;AACrC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAChC,KAAQ,EACR,CAAiD;IAEjD,MAAM,MAAM,GAAG,oBAAoB,CAAC,KAAK,CAAC,CAAC;IAE3C,MAAM,MAAM,GAAG,mBAAmB,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;IAEpD,OAAO,MAA4B,CAAC;AACrC,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport type { TreeLeafValue, TreeNode } from \"@fluidframework/tree\";\nimport { Tree } from \"@fluidframework/tree\";\nimport { TreeAlpha } from \"@fluidframework/tree/internal\";\nimport {\n\ttype FC,\n\tmemo,\n\ttype MemoExoticComponent,\n\ttype ReactNode,\n\tuseEffect,\n\tuseState,\n} from \"react\";\n\nimport {\n\tunwrapPropTreeNode,\n\tunwrapPropTreeRecord,\n\ttype PropTreeNodeRecord,\n\ttype PropTreeValue,\n\ttype UnwrapPropTreeNodeRecord,\n\ttype WrapNodes,\n} from \"./propNode.js\";\nimport { useObservation, type ObservationOptions } from \"./useObservation.js\";\n\n/**\n * Custom hook which invalidates a React Component when there is a change in the subtree defined by `subtreeRoot`.\n * This includes changes to the tree's content, but not changes to its parentage.\n * See {@link @fluidframework/tree#TreeChangeEvents.treeChanged} for details.\n * @remarks\n * Consider using {@link useTreeObservations} instead which tracks what was observed and only invalidates if it changes.\n * @alpha\n */\nexport function useTree(subtreeRoot: TreeNode): number {\n\t// Use a React effect hook to invalidate this component when the subtreeRoot changes.\n\t// We do this by incrementing a counter, which is passed as a dependency to the effect hook.\n\tconst [invalidations, setInvalidations] = useState(0);\n\n\t// React effect hook that increments the 'invalidation' counter whenever subtreeRoot or any of its children change.\n\tuseEffect(() => {\n\t\t// Returns the cleanup function to be invoked when the component unmounts.\n\t\treturn Tree.on(subtreeRoot, \"treeChanged\", () => {\n\t\t\tsetInvalidations((i) => i + 1);\n\t\t});\n\t}, [invalidations, subtreeRoot]);\n\n\treturn invalidations;\n}\n\n/**\n * Higher order component which wraps a component to use {@link useTreeObservations}.\n *\n * @remarks\n * When passing TreeNodes in props, care must be taken to not observe their content outside of a context which does observation tracking (or manual invalidation).\n * This wraps a component in such tracking.\n *\n * It is recommended that sub-components which take in TreeNodes, if not defined using this higher order components, take the nodes in as {@link PropTreeNode}s.\n * Components defined using this higher order component can take in either raw TreeNodes or {@link PropTreeNode}s: the latter will be automatically unwrapped.\n * @privateRemarks\n * `FC` does not seem to be covariant over its input type, so to make use of this more ergonomic,\n * the return type intersects the various ways this could be used (with or without PropTreeNode wrapping).\n * @alpha\n */\nexport function withTreeObservations<TIn>(\n\tcomponent: FC<TIn>,\n\toptions?: ObservationOptions,\n): FC<TIn> & FC<WrapNodes<TIn>> & FC<TIn | WrapNodes<TIn>> {\n\treturn (props: TIn | WrapNodes<TIn>): ReactNode =>\n\t\tuseTreeObservations(() => component(props as TIn), options);\n}\n\n/**\n * {@link withTreeObservations} wrapped with memo.\n * @remarks\n * There is no special logic here, just a convenience wrapper.\n * @alpha\n */\nexport function withMemoizedTreeObservations<TIn>(\n\tcomponent: FC<TIn>,\n\toptions?: ObservationOptions & {\n\t\treadonly propsAreEqual?: Parameters<typeof memo>[1];\n\t},\n): MemoExoticComponent<ReturnType<typeof withTreeObservations<TIn>>> {\n\treturn memo(withTreeObservations(component, options), options?.propsAreEqual);\n}\n\n/**\n * Custom hook which invalidates a React Component when there is a change in tree content observed during `trackDuring`.\n *\n * @param trackDuring - Called synchronously, and will have its tree observations tracked.\n *\n * @remarks\n * This includes changes to the tree's content.\n * Currently this will throw if observing a node's parentage to be undefined,\n * and node status changes will not cause invalidation.\n *\n * For additional type safety to help avoid observing TreeNode content outside of this hook, see {@link PropTreeNode}.\n * @alpha\n */\nexport function useTreeObservations<TResult>(\n\ttrackDuring: () => TResult,\n\toptions?: ObservationOptions,\n): TResult {\n\treturn useObservation(\n\t\t(invalidate) => TreeAlpha.trackObservationsOnce(invalidate, trackDuring),\n\t\toptions,\n\t);\n}\n\n/**\n * Custom hook for using a prop tree node.\n *\n * @param propNode - Input, automatically unwrapped TreeNode from a {@link PropTreeNode} if needed.\n * @param trackDuring - Callback which reads from the node and returns a result.\n * If the result is a TreeNode or {@link NodeRecord} it will be wrapped as a {@link PropTreeNode} or {@link PropTreeNodeRecord}, see {@link WrapNodes}.\n *\n * It is recommended that when returning data containing TreeNodes,\n * use a format supported by {@link WrapNodes} or wrap the nodes manually using {@link toPropTreeNode}.\n * This improves the type safety, reducing the risk of invalidation bugs due to untracked access of tree content contained in the return value.\n *\n * Note that is is fine to observe any node inside the callback, not just the provided node: all accesses will be tracked.\n * The input node is just provided as a way to automatically unwrap the {@link PropTreeNode}\n *\n * @remarks\n * Reads content using {@link useTreeObservations} to track dependencies.\n * @alpha\n */\nexport function usePropTreeNode<T extends TreeNode | TreeLeafValue, TResult>(\n\tpropNode: PropTreeValue<T> | T,\n\ttrackDuring: (node: T) => TResult,\n): WrapNodes<TResult> {\n\tconst node: T = unwrapPropTreeNode(propNode);\n\n\tconst result = useTreeObservations(() => trackDuring(node));\n\n\treturn result as WrapNodes<TResult>;\n}\n\n/**\n * {@link usePropTreeNode} but takes in a {@link PropTreeNodeRecord}.\n * @alpha\n */\nexport function usePropTreeRecord<const T extends PropTreeNodeRecord, TResult>(\n\tprops: T,\n\tf: (node: UnwrapPropTreeNodeRecord<T>) => TResult,\n): WrapNodes<TResult> {\n\tconst record = unwrapPropTreeRecord(props);\n\n\tconst result = useTreeObservations(() => f(record));\n\n\treturn result as WrapNodes<TResult>;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluidframework/react",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.92.0",
|
|
4
4
|
"description": "Utilities for integrating content powered by the Fluid Framework into React applications",
|
|
5
5
|
"homepage": "https://fluidframework.com",
|
|
6
6
|
"repository": {
|
|
@@ -63,28 +63,26 @@
|
|
|
63
63
|
"temp-directory": "nyc/.nyc_output"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"@fluidframework/aqueduct": "~2.
|
|
67
|
-
"@fluidframework/core-interfaces": "~2.
|
|
68
|
-
"@fluidframework/core-utils": "~2.
|
|
69
|
-
"@fluidframework/datastore-definitions": "~2.
|
|
70
|
-
"@fluidframework/fluid-static": "~2.
|
|
71
|
-
"@fluidframework/runtime-definitions": "~2.
|
|
72
|
-
"@fluidframework/shared-object-base": "~2.
|
|
73
|
-
"@fluidframework/tree": "~2.
|
|
74
|
-
"quill": "^2.0.3",
|
|
75
|
-
"quill-delta": "^5.1.0",
|
|
66
|
+
"@fluidframework/aqueduct": "~2.92.0",
|
|
67
|
+
"@fluidframework/core-interfaces": "~2.92.0",
|
|
68
|
+
"@fluidframework/core-utils": "~2.92.0",
|
|
69
|
+
"@fluidframework/datastore-definitions": "~2.92.0",
|
|
70
|
+
"@fluidframework/fluid-static": "~2.92.0",
|
|
71
|
+
"@fluidframework/runtime-definitions": "~2.92.0",
|
|
72
|
+
"@fluidframework/shared-object-base": "~2.92.0",
|
|
73
|
+
"@fluidframework/tree": "~2.92.0",
|
|
76
74
|
"react": "^18.3.1",
|
|
77
75
|
"react-dom": "^18.3.1"
|
|
78
76
|
},
|
|
79
77
|
"devDependencies": {
|
|
80
78
|
"@arethetypeswrong/cli": "^0.18.2",
|
|
81
|
-
"@biomejs/biome": "~
|
|
82
|
-
"@fluid-internal/mocha-test-setup": "~2.
|
|
83
|
-
"@fluid-tools/build-cli": "^0.
|
|
79
|
+
"@biomejs/biome": "~2.4.5",
|
|
80
|
+
"@fluid-internal/mocha-test-setup": "~2.92.0",
|
|
81
|
+
"@fluid-tools/build-cli": "^0.64.0",
|
|
84
82
|
"@fluidframework/build-common": "^2.0.3",
|
|
85
|
-
"@fluidframework/build-tools": "^0.
|
|
86
|
-
"@fluidframework/eslint-config-fluid": "
|
|
87
|
-
"@fluidframework/tinylicious-client": "~2.
|
|
83
|
+
"@fluidframework/build-tools": "^0.64.0",
|
|
84
|
+
"@fluidframework/eslint-config-fluid": "^9.0.0",
|
|
85
|
+
"@fluidframework/tinylicious-client": "~2.92.0",
|
|
88
86
|
"@microsoft/api-extractor": "7.52.11",
|
|
89
87
|
"@testing-library/react": "^16.3.0",
|
|
90
88
|
"@types/mocha": "^10.0.10",
|
|
@@ -99,6 +97,7 @@
|
|
|
99
97
|
"eslint-config-prettier": "~10.1.8",
|
|
100
98
|
"global-jsdom": "^26.0.0",
|
|
101
99
|
"jiti": "^2.6.1",
|
|
100
|
+
"jsdom": "^26.1.0",
|
|
102
101
|
"mocha": "^11.7.5",
|
|
103
102
|
"mocha-multi-reporters": "^1.5.1",
|
|
104
103
|
"rimraf": "^6.1.3",
|
package/react.test-files.tar
CHANGED
|
Binary file
|
package/src/index.ts
CHANGED
|
@@ -45,13 +45,5 @@ export {
|
|
|
45
45
|
withMemoizedTreeObservations,
|
|
46
46
|
} from "./useTree.js";
|
|
47
47
|
export { objectIdNumber } from "./simpleIdentifier.js";
|
|
48
|
-
|
|
49
|
-
export {
|
|
50
|
-
FormattedMainView,
|
|
51
|
-
PlainTextMainView,
|
|
52
|
-
PlainQuillView,
|
|
53
|
-
type FormattedMainViewProps,
|
|
54
|
-
type PlainMainViewProps,
|
|
55
|
-
type FormattedEditorHandle,
|
|
56
|
-
} from "./text/index.js";
|
|
48
|
+
export { syncTextToTree, PlainTextMainView } from "./text/index.js";
|
|
57
49
|
export { UndoRedoStacks, type UndoRedo } from "./undoRedo.js";
|
|
@@ -20,7 +20,7 @@ import type {
|
|
|
20
20
|
InsertableTreeFieldFromImplicitField,
|
|
21
21
|
} from "@fluidframework/tree";
|
|
22
22
|
import { configuredSharedTree, FormatValidatorBasic } from "@fluidframework/tree/internal";
|
|
23
|
-
import
|
|
23
|
+
import { type FC, type JSX, useCallback, useEffect, useState } from "react";
|
|
24
24
|
|
|
25
25
|
import { toPropTreeNode, type PropTreeValue } from "./propNode.js";
|
|
26
26
|
|
|
@@ -138,7 +138,7 @@ export interface IReactTreeDataObject<TSchema extends ImplicitFieldSchema> {
|
|
|
138
138
|
* and thus making it a member avoids the user of this from having to explicitly provide the type parameter.
|
|
139
139
|
* This is an arrow function not a method so it gets the correct this when not called as a member.
|
|
140
140
|
*/
|
|
141
|
-
readonly TreeViewComponent: (props: TreeViewProps<TSchema>) =>
|
|
141
|
+
readonly TreeViewComponent: (props: TreeViewProps<TSchema>) => JSX.Element;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
/**
|
|
@@ -149,7 +149,7 @@ export interface TreeViewProps<TSchema extends ImplicitFieldSchema> {
|
|
|
149
149
|
/**
|
|
150
150
|
* Component to display the tree content.
|
|
151
151
|
*/
|
|
152
|
-
readonly viewComponent:
|
|
152
|
+
readonly viewComponent: FC<{
|
|
153
153
|
root: PropTreeValue<TreeFieldFromImplicitField<TSchema>>;
|
|
154
154
|
}>;
|
|
155
155
|
|
|
@@ -159,7 +159,7 @@ export interface TreeViewProps<TSchema extends ImplicitFieldSchema> {
|
|
|
159
159
|
*
|
|
160
160
|
* @defaultValue Component which describes the situation (in English) and allows the user to upgrade the schema to match the {@link @fluidframework/tree#TreeViewConfiguration} if possible.
|
|
161
161
|
*/
|
|
162
|
-
readonly errorComponent?:
|
|
162
|
+
readonly errorComponent?: FC<SchemaIncompatibleProps>;
|
|
163
163
|
|
|
164
164
|
// TODO: Once its possible to query the status of individual schema upgrades, provide more options here for handling such cases.
|
|
165
165
|
}
|
|
@@ -203,11 +203,11 @@ export abstract class ReactTreeDataObject<
|
|
|
203
203
|
function useViewCompatibility<TSchema extends ImplicitFieldSchema>(
|
|
204
204
|
view: TreeView<TSchema>,
|
|
205
205
|
): SchemaCompatibilityStatus {
|
|
206
|
-
const [compatibility, setCompatibility] =
|
|
206
|
+
const [compatibility, setCompatibility] = useState<SchemaCompatibilityStatus>(
|
|
207
207
|
view.compatibility,
|
|
208
208
|
);
|
|
209
209
|
|
|
210
|
-
|
|
210
|
+
useEffect(() => {
|
|
211
211
|
const updateCompatibility = (): void => {
|
|
212
212
|
setCompatibility(view.compatibility);
|
|
213
213
|
};
|
|
@@ -222,11 +222,9 @@ function useViewCompatibility<TSchema extends ImplicitFieldSchema>(
|
|
|
222
222
|
function useViewRoot<TSchema extends ImplicitFieldSchema>(
|
|
223
223
|
view: TreeView<TSchema>,
|
|
224
224
|
): TreeFieldFromImplicitField<TSchema> | undefined {
|
|
225
|
-
const [root, setRoot] =
|
|
226
|
-
undefined,
|
|
227
|
-
);
|
|
225
|
+
const [root, setRoot] = useState<TreeFieldFromImplicitField<TSchema> | undefined>(undefined);
|
|
228
226
|
|
|
229
|
-
|
|
227
|
+
useEffect(() => {
|
|
230
228
|
const updateRoot = (): void => {
|
|
231
229
|
if (view.compatibility.canView) {
|
|
232
230
|
setRoot(view.root);
|
|
@@ -253,12 +251,12 @@ export function TreeViewComponent<TSchema extends ImplicitFieldSchema>({
|
|
|
253
251
|
errorComponent,
|
|
254
252
|
}: TreeViewProps<TSchema> & {
|
|
255
253
|
tree: Pick<IReactTreeDataObject<TSchema>, "treeView">;
|
|
256
|
-
}):
|
|
254
|
+
}): JSX.Element {
|
|
257
255
|
const view = tree.treeView;
|
|
258
256
|
|
|
259
257
|
const compatibility = useViewCompatibility(view);
|
|
260
258
|
const root = useViewRoot(view);
|
|
261
|
-
const upgradeSchema =
|
|
259
|
+
const upgradeSchema = useCallback((): void => view.upgradeSchema(), [view]);
|
|
262
260
|
|
|
263
261
|
// Note: this policy is on the stricter side and ensures that clients will only be able to submit edits when their view schema
|
|
264
262
|
// supports exactly the same documents as the stored schema.
|
|
@@ -302,7 +300,7 @@ function TreeErrorComponent({
|
|
|
302
300
|
}: {
|
|
303
301
|
compatibility: SchemaCompatibilityStatus;
|
|
304
302
|
upgradeSchema: () => void;
|
|
305
|
-
}):
|
|
303
|
+
}): JSX.Element {
|
|
306
304
|
// eslint-disable-next-line unicorn/prefer-ternary
|
|
307
305
|
if (compatibility.canUpgrade) {
|
|
308
306
|
return (
|