Search code examples
reactjsreact-hookscytoscape.js

How to create a Functional React Component using Cytoscape.js?


I am working with Cytoscape.js and React. I have a functional GraphComponent that receives the list of elements used to render the Cytoscape graph, and a list of nodes I wish to animate.

The problem I have is that I cannot figure out how to persist the 'cy' object so that I may access it at a later time. Specifically, I would like to add a class to the nodes in path.

const MyComponent = ({ myElements, path }) => {
    
    useEffect(() => {

        const cy = {
            ...
            elements: myElements
            ...
        }

    }, []);

    
    useEffect(() => {

        // This fails since 'cy' is undefined
        cy.$id("A").addClass("highlighted");

    }, [path]);

    return (
        <div id="cy" />
    );
}

Idea #1: Create a variable 'cy' that I can then set the value to in useEffect.

Problem: After the initial render the value of 'cy' is lost

const MyComponent = ({ myElements, path }) => {

    let cy;
    
    useEffect(() => {

        cy = {
            ...
            elements: myElements
            ...
        }

    }, []);

    useEffect(() => {

        // This fails since 'cy' is undefined
        cy.$id("A").addClass("highlighted");

    }, [path]);

    return (
        <div id="cy" />
    );
}

Idea #2: Save cy as state.

Problem: Cy is not properly initialized

const MyComponent = ({ myElements, path }) => {

    const [cy, setCy] = useState({});
    
    useEffect(() => {

        setCy({
            ...
            elements: myProp
            ...
        });

    }, []);

    useEffect(() => {

        cy.$id("A").addClass("highlighted");

    }, [path]);

    return (

        // This fails since cy is not initialized to a cytoscape object when component mounts
        <div id="cy" />
    );
}

So the question is, how do I create and access the cytoscape object (and modify its nodes and elements) inside a react functional component?


Solution

  • You normally don't want to store props in state, because you're already storing that somewhere else. If you're passing in the data for cy via a prop, just use the prop.

    In general, if you're trying to keep state updated based on external changes, you need to add a dependency to your useEffect hook. An empty array means the effect will only run once, on mount.

    Refer to the docs for a good tutorial, but generally it works like this:

    useEffect(()=> {
       setSomeState(/* set state */)
    }, [someValue])
    

    If someValue stays the same, that effect won't re-run. If it changes, the effect will run the next time there's a render (due to a prop or state change).

    Again, if the data is coming in via a prop, don't set it in state. If you want to track something internal to the component that's based off of that prop, then useEffect with the dependency on the prop will solve that for you.

    Edit for comments

    State is where you save data between renderings. If the problem is that you shouldn't render because a value isn't there on mount (but will be in another render), then wrap it in an expression, e.g.,

    {someValueOrExpression ? <div id="cy" /> : <div>Empty State</div>}

    or

    {someValueOrExpression && <div id="cy" />}

    That way the div won't show and fail until it has what it needs to render properly.