Skip to content

[react-integration] update to react 18 and useSyncExternalStore #96

Description

@graphicore

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions