reactjsreact-vis

How can I induce rerendering of other components on an update to this mutable ref to Vis.js network? Is there a better package available?


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.)


Solution

  • 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.