I had a chat with Google Gemini, it suggested this:
Your current useMetamodel and useMetamodelSimple hooks use the traditional useState + useEffect subscription pattern:
const [stateValue, setStateValue] = useState(() => widgetBus.getEntry(path).value);
useEffect(() => {
return widgetBus.addListener((changedMap) => setStateValue(...));
}, [...]);
The Problem: In React 18+ (Concurrent Mode), using useEffect to subscribe to external state causes tearing. React might pause a render halfway through, the metamodel updates, and React resumes the render using two different versions of the state for different components on the same screen.
The Solution: React introduced a dedicated hook specifically for libraries like yours: useSyncExternalStore. It forces React to read synchronously from your metamodel without tearing, and it completely eliminates the need for useEffect and useState.
Here is how you rewrite your hook to the modern React 18 standard. It actually makes your code shorter and cleaner:
import { useSyncExternalStore, useCallback, useContext } from "react";
export function useMetamodelSimple(path) {
const widgetBus = useContext(WidgetContext);
if (!widgetBus) {
throw new Error("useMetamodelSimple must be used within a WidgetBus component");
}
// 1. React 18's native bridge for external state managers
const stateValue = useSyncExternalStore(
// Subscribe function: React calls this to listen for changes
(callback) => {
const externalName = widgetBus.getExternalName(path);
// Whenever the path changes, just tell React to re-render.
return widgetBus.addListener(callback, [[externalName, path]]);
},
// Get Snapshot function: React calls this to get the current value
() => widgetBus.getEntry(path).value
);
// 2. The Setter (Unchanged, already perfect)
const setValue = useCallback(
(newValue) => widgetBus.changeState(() => {
widgetBus.getEntry(path).value = newValue;
}),
[widgetBus, path]
);
return [stateValue, setValue];
}
(…)
If you apply the useSyncExternalStore upgrade, your React bindings will be mathematically perfect—immune to Concurrent Mode bugs, fully immune to unnecessary re-renders, and frictionless for UI engineers to use.
I had a chat with Google Gemini, it suggested this:
Your current useMetamodel and useMetamodelSimple hooks use the traditional useState + useEffect subscription pattern:
The Problem: In React 18+ (Concurrent Mode), using useEffect to subscribe to external state causes tearing. React might pause a render halfway through, the metamodel updates, and React resumes the render using two different versions of the state for different components on the same screen.
The Solution: React introduced a dedicated hook specifically for libraries like yours: useSyncExternalStore. It forces React to read synchronously from your metamodel without tearing, and it completely eliminates the need for useEffect and useState.
Here is how you rewrite your hook to the modern React 18 standard. It actually makes your code shorter and cleaner:
(…)
If you apply the useSyncExternalStore upgrade, your React bindings will be mathematically perfect—immune to Concurrent Mode bugs, fully immune to unnecessary re-renders, and frictionless for UI engineers to use.