diff --git a/package-lock.json b/package-lock.json
index 520f8df..7a5f800 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6724,6 +6724,15 @@
"integrity": "sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==",
"dev": true
},
+ "node_modules/@types/lodash.debounce": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz",
+ "integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/lodash": "*"
+ }
+ },
"node_modules/@types/lodash.merge": {
"version": "4.6.9",
"resolved": "https://registry.npmjs.org/@types/lodash.merge/-/lodash.merge-4.6.9.tgz",
@@ -18562,6 +18571,7 @@
"deep-equal": "^2.2.3",
"esbuild-plugin-flow": "github:dalcib/esbuild-plugin-flow",
"immer": "^10.1.1",
+ "lodash.debounce": "^4.0.8",
"lodash.merge": "^4.6.2",
"react": "^18.3.1",
"react-art": "^18.3.1",
@@ -18582,6 +18592,7 @@
"@types/chrome": "^0.0.270",
"@types/deep-equal": "^1.0.4",
"@types/jest": "^29.5.13",
+ "@types/lodash.debounce": "^4.0.9",
"@types/lodash.merge": "^4.6.9",
"@types/node": "^22.5.4",
"@types/react": "^18.3.5",
@@ -18875,6 +18886,7 @@
"classnames": "^2.3.2",
"cytoscape": "^3.30.4",
"cytoscape-elk": "^2.2.0",
+ "lodash.debounce": "^4.0.8",
"lodash.throttle": "^4.1.1",
"react": "^18.2.0",
"react-cytoscapejs": "^2.0.0",
@@ -18891,6 +18903,7 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.1",
"@types/cytoscape": "^3.19.11",
+ "@types/lodash.debounce": "^4.0.9",
"@types/lodash.throttle": "^4.1.9",
"@types/react": "^18.2.15",
"babel-jest": "^29.7.0",
diff --git a/packages/browser-extension/package.json b/packages/browser-extension/package.json
index 02e8e94..a21847b 100644
--- a/packages/browser-extension/package.json
+++ b/packages/browser-extension/package.json
@@ -31,14 +31,15 @@
"dependencies": {
"@reduxjs/toolkit": "^2.2.7",
"@sentry/browser": "^8.33.1",
+ "@sophistree/common": "file:../common",
+ "@sophistree/ui-common": "file:../ui-common",
"classnames": "^2.5.1",
"cytoscape": "^3.30.2",
"cytoscape-elk": "^2.2.0",
"deep-equal": "^2.2.3",
"esbuild-plugin-flow": "github:dalcib/esbuild-plugin-flow",
- "@sophistree/common": "file:../common",
- "@sophistree/ui-common": "file:../ui-common",
"immer": "^10.1.1",
+ "lodash.debounce": "^4.0.8",
"lodash.merge": "^4.6.2",
"react": "^18.3.1",
"react-art": "^18.3.1",
@@ -59,6 +60,7 @@
"@types/chrome": "^0.0.270",
"@types/deep-equal": "^1.0.4",
"@types/jest": "^29.5.13",
+ "@types/lodash.debounce": "^4.0.9",
"@types/lodash.merge": "^4.6.9",
"@types/node": "^22.5.4",
"@types/react": "^18.3.5",
diff --git a/packages/browser-extension/src/components/EntityEditor.tsx b/packages/browser-extension/src/components/EntityEditor.tsx
index a0f7fcc..4fef687 100644
--- a/packages/browser-extension/src/components/EntityEditor.tsx
+++ b/packages/browser-extension/src/components/EntityEditor.tsx
@@ -1,7 +1,8 @@
-import React from "react";
+import React, { useMemo, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { TextInput, Text, Surface } from "react-native-paper";
import { View, StyleSheet } from "react-native";
+import debounce from "lodash.debounce";
import {
Polarity,
@@ -47,15 +48,28 @@ function chooseEditor(entity: Entity) {
function PropositionEditor({ entity }: { entity: Proposition }) {
const dispatch = useDispatch();
+ const [text, setText] = useState(entity.text);
+
+ const debouncedDispatch = useMemo(
+ () =>
+ debounce((text: string) => {
+ dispatch(updateProposition({ id: entity.id, updates: { text } }));
+ }, 150),
+ [dispatch, entity.id],
+ );
+
+ const handleTextChange = (newText: string) => {
+ setText(newText);
+ debouncedDispatch(newText);
+ };
+
return (
- dispatch(updateProposition({ id: entity.id, updates: { text } }))
- }
+ onChangeText={handleTextChange}
/>
);
@@ -84,22 +98,35 @@ function JustificationEditor({ entity }: { entity: Justification }) {
function MediaExcerptEditor({ entity }: { entity: MediaExcerpt }) {
const dispatch = useDispatch();
+ const [text, setText] = useState(entity.sourceInfo.name);
+
+ const debouncedDispatch = useMemo(
+ () =>
+ debounce((text: string) => {
+ dispatch(
+ updateMediaExerpt({
+ id: entity.id,
+ updates: { sourceInfo: { name: text } },
+ }),
+ );
+ }, 150),
+ [dispatch, entity.id],
+ );
+
+ const handleTextChange = (newText: string) => {
+ setText(newText);
+ debouncedDispatch(newText);
+ };
+
return (
- dispatch(
- updateMediaExerpt({
- id: entity.id,
- updates: { sourceInfo: { name: text } },
- }),
- )
- }
+ onChangeText={handleTextChange}
/>
Quotation
diff --git a/packages/ui-common/package.json b/packages/ui-common/package.json
index f7e796e..7643ed7 100644
--- a/packages/ui-common/package.json
+++ b/packages/ui-common/package.json
@@ -20,6 +20,7 @@
"classnames": "^2.3.2",
"cytoscape": "^3.30.4",
"cytoscape-elk": "^2.2.0",
+ "lodash.debounce": "^4.0.8",
"lodash.throttle": "^4.1.1",
"react": "^18.2.0",
"react-cytoscapejs": "^2.0.0",
@@ -36,6 +37,7 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.1",
"@types/cytoscape": "^3.19.11",
+ "@types/lodash.debounce": "^4.0.9",
"@types/lodash.throttle": "^4.1.9",
"@types/react": "^18.2.15",
"babel-jest": "^29.7.0",
diff --git a/packages/ui-common/src/cytoscape/reactNodes.tsx b/packages/ui-common/src/cytoscape/reactNodes.tsx
index 607d749..8279466 100644
--- a/packages/ui-common/src/cytoscape/reactNodes.tsx
+++ b/packages/ui-common/src/cytoscape/reactNodes.tsx
@@ -5,6 +5,7 @@ import cytoscape, {
} from "cytoscape";
import ReactDOM from "react-dom/client";
import throttle from "lodash.throttle";
+import debounce from "lodash.debounce";
import { sunflower } from "../colors";
@@ -23,6 +24,8 @@ interface ReactNodesOptions {
layoutOptions: LayoutOptions;
// The delay before reactNodes applies a layout when one is necessary.
layoutThrottleDelay?: number;
+ // The delay before rendering the JSX after the node data changes.
+ nodeDataRenderDelay?: number;
logger: Logger;
}
@@ -37,10 +40,14 @@ export interface ReactNodeOptions {
unselectedStyle?: Partial;
}
-const defaultOptions: Required> =
- {
- layoutThrottleDelay: 100,
- };
+type PassthroughOptions = Pick;
+
+const defaultOptions: Required<
+ Pick
+> = {
+ layoutThrottleDelay: 500,
+ nodeDataRenderDelay: 150,
+};
const defaultReactNodeOptions: ReactNodeOptions = {
query: "node", // selector for nodes to apply HTML to
@@ -65,8 +72,14 @@ function reactNodes(this: cytoscape.Core, options: ReactNodesOptions) {
this.layout(options.layoutOptions).run();
}, options.layoutThrottleDelay ?? defaultOptions.layoutThrottleDelay);
+ const { nodeDataRenderDelay } = options;
options.nodes.forEach((nodeOptions) =>
- makeReactNode(this, options.logger, nodeOptions, layout),
+ makeReactNode(
+ this,
+ options.logger,
+ { ...nodeOptions, nodeDataRenderDelay },
+ layout,
+ ),
);
applyWebkitLayoutWorkaround(this, options.layoutOptions);
@@ -111,7 +124,7 @@ function applyWebkitLayoutWorkaround(
function makeReactNode(
cy: cytoscape.Core,
logger: Logger,
- options: ReactNodeOptions,
+ options: ReactNodeOptions & PassthroughOptions,
layout: () => void,
) {
options = Object.assign({}, defaultReactNodeOptions, options);
@@ -170,9 +183,14 @@ function makeReactNode(
Object.assign(htmlElement.style, options.unselectedStyle);
}
});
- node.on("data", function renderReactNode() {
+
+ // Debounce the node data event handler to reduce Redux updates
+ const debouncedDataHandler = debounce(function renderReactNode() {
renderJsxElement(reactRoot);
- });
+ }, options.nodeDataRenderDelay);
+
+ node.on("data", debouncedDataHandler);
+
if (options.syncClasses) {
node.on("style", syncNodeClasses);
}