1

I'm trying to work with Vis.js in React and have started with the pattern shared by James Tharpe: https://www.jamestharpe.com/react-visjs/.

I'm also trying to include what I'm calling BoltOns which take the state of the network as a param, allowing them to manipulate the event handlers as needed.

Unfortunately, theses BoltOns don't seem to respond to state updates in the way I would expect and therefore I can't properly set the event handlers. My problems are noted in the comments below. Generally, it seems that the object being acted on by the BoltOns is one update behind. It seems it should be able to work this way, as if I trigger my dev server to recompile the behavior is as expected.

export const NetworkDiagram : FC<NetworkDiagramProps>  = ({
    nodes,
    edges,
    BoltOns = [DefaultNetworkDiagramToolbar, NetworkDiagramEditor],
    options
}) =>{

    // Forces an update.
    // Despite using memo below,
    // this causes a circular update.
    const [tick, forceUpdate] = useReducer(x=>x+1, 0);

    // A reference to the div rendered by this component
    const domNode = useRef<HTMLDivElement>(null);

    // A reference to the vis network instance
    const network = useRef<Network | null>(null);

    // An array of nodes
    const _nodes = convertNodesToDataSet(nodes||{});

    const data: Data = {
        nodes  : _nodes,
        edges : edges
    };


    useEffect(() => {
        if (domNode.current) {
            network.current = new Network(domNode.current, data, {});
            network.current.setOptions({
                physics : true,
                height : "100%",
                width : "100%",
                ...options
            });
            forceUpdate();
            // This also fails if I limit the number of ticks to 5.
            // The ref ref received still does not properly allow me to set the event
            // handlers.
            // I have also tried 
            // if(!initLoad) setInitLoad(true);
            // but this produces the same behavior as limiting the number of ticks.
        }
    }, [domNode, network, data, options]);

    return (
        <div style={{
            position : "relative",
            height : "100vh",
            width : "100vw"
        }}>
            {BoltOns.map((BoltOn)=><BoltOn updateCount={tick} network={network} domNode={domNode}/>)}
            {useMemo(()=><div style={{
                height : "100vh",
                width : "100vw"
            }} ref={domNode}/>, [domNode, network, data, options])}
        </div>
    );

}

Can anyone suggest an alternative pattern, or perhaps network library? (I've already made two moves.)

lmonninger
  • 831
  • 3
  • 13
  • Remember that you're not working with the DOM event model when using React: in the DOM model, elements update and those updates generate events which you can respond to, and if you don't they are processed as per whatever the default behaviour is. React works literally the other way around: user events happen, _but the elements themselves are never notified of that_, the events are aborted immediately and instead your React code may trigger state/prop updates. Those may then trigger render passes, and then _that_ may lead to updated elements. – Mike 'Pomax' Kamermans Dec 26 '21 at 17:21
  • @Mike'Pomax'Kamermans So, I should abandon any attempts at using event listeners to trigger state updates? (This has worked for me in previous iterations when not dealing with BoltOns.) If this is the case, how have other conversions of Vis.js (or canvas based renderers generally) to React worked? – lmonninger Dec 26 '21 at 17:48
  • That strongly depends on what "BoltOns" are in this case - it's not something I've heard of before, and a casual google for "react boltons" and "javascript boltons" doesn't find anything useful. As it also doesn't a tag associated with it: can you explain in your post what that terms means? A sign that something probably-bad-and-you-probably-want-to-rearchitect-this-at-least-a-little is going on is that you're relying on `forceUpdate()` instead of state updates. While that function exists, you really shouldn't ever need to call it. – Mike 'Pomax' Kamermans Dec 26 '21 at 17:54
  • @Mike'Pomax'Kamermans I'm not using "BoltOn" in any kind of formal way. I just wanted to distinguish between components that are intended to interact only with React state (AddOns as I'm calling them) and components that are intended to interact with the Vis.js Network object itself. In other words, components that will receive only React state vs. components that receive the network components. Note: Even if I get this to work, I'd still be rather worried about race conditions. So, some other pattern is really what I'm after. – lmonninger Dec 26 '21 at 17:58
  • I would strongly suggest using conventional terms then, using "BoltOns" suggests you're using a specific library or methodology that uses that term. So please add the description you just wrote a comment about [in your post, instead](/help/how-to-ask) because that's not something to tell "me", that's something to tell "everybody" =) – Mike 'Pomax' Kamermans Dec 26 '21 at 18:57

1 Answers1

0

After some trial and error, the below ended up working best for me.

export interface NetworkDiagramProps{
    nodes ? : {[key : string] : NodeDetailsI}
    edges ? : Edge[],
    BoltOns ? : NetworkDiagramBoltOn[]
    options ? : Options,
    extractNetwork ? : (network ? : Network)=>any
}

/**
 * A React
 * @param param0 
 * @returns 
 */
export const NetworkDiagram : FC<NetworkDiagramProps>  = ({
    nodes,
    edges,
    BoltOns = [DefaultNetworkDiagramToolbar, NetworkDiagramEditor],
    options,
    extractNetwork
}) =>{

     const network = useRef<Network|undefined>(undefined);

    // A reference to the div rendered by this component
    const domNode = useRef<HTMLDivElement>(null);

    // I'm only accepting nodes as an object, with keys for ids.
    const _nodes = convertNodesToDataSet(nodes||{});

    const data = {
        nodes  : _nodes,
        edges : new DataSet(edges||[])
    };

    useEffect(()=>{
        if(domNode.current && !network.current) network.current = new Network(domNode.current, data, {});
    }, [domNode.current])


    // We need the component to be rerendered once
    // after the domNode has been rendered and the network initialized.
    const [tick, forceUpdate] = useReducer(x=>x+1, 0);
    useEffect(()=>{
        forceUpdate();
    }, [])
    
    // handle new data on options by mutably setting the data and options
    useEffect(()=>{
        network.current?.setData(data);
    }, [data, tick])
    useEffect(()=>{
        network.current?.setOptions(options||{});
    }, [options, tick])

    // allow network to be extracted
    useEffect(()=>{
        extractNetwork && extractNetwork(network.current);
    }, [tick])

    // And, the teardown
    useEffect(()=>{
        return ()=>{
            if(network.current){
                network.current.destroy();
                delete network.current;
            }
        }
    }, [])

    return (
        <div style={{
            position : "relative",
            ...style
        }}>
            {useMemo(()=><div style={{
                height : "100%",
                width : "100%"
            }} ref={domNode}/>, [domNode])}
            {BoltOns.map((BoltOn)=><BoltOn
            edges={data.edges}
            nodes={data.nodes}
            network={network.current}/>)}
        </div>
    );
}

I haven't encountered any unexpected behavior while using this approach thusfar.

I also believe rendering once and communicating state updates as mutations should improve performance by skipping network initialization. That being said, I haven't written any performance tests yet.

lmonninger
  • 831
  • 3
  • 13