skip to Main Content

Im using D3 with react useEffect hook and i have click method wrapped with useCallback hook and its updating the state, but when I’m clicking the element state is not updating at all. To stop re-rendering the D3 SVG element I don’t want to add counter as dependency to useEffect hook.

Is their way I can update the counter state without adding counter on useEffect dependency?

CodeSandbbox

import { useEffect, useState, useCallback } from "react";
import * as d3 from "d3";

export default function App() {
  const [counter, setCounter] = useState(0);

  const handleClick = useCallback(() => {
    setCounter(counter + 1);
      console.log(counter);
  }, [counter]); 

  useEffect(() => {
    const svg = d3.select("#svg");

    //naming the selection
    const redRect = svg
      .append("rect")
      .attr("x", 0)
      .attr("y", 0)
      .attr("width", 300)
      .attr("height", 300)
      .attr("fill", "red")
      .attr("id", "red-rect");

    redRect.on("click", handleClick);
  }, []); // should not re-render the d3 with counter as the dependency 

  return <svg id="svg" />;
}

2

Answers


  1. Chosen as BEST ANSWER

    I have found the way to block useEffect redo all the D3 things with useRef, I use the useRef value as D3 SVG rect then I'm checking if rectRef.current exists then ignore the D3 methods and bind the handleClick to rectRef.current its working.

    import { useEffect, useState, useCallback, useRef } from "react";
    import * as d3 from "d3";
    
    let calls = 0;
    
    console.log();
    
    export default function App() {
      const [counter, setCounter] = useState(0);
    
      const rectRef = useRef(null);
    
      const handleClick = useCallback(() => {
        setCounter(counter + 1);
      }, [counter]);
    
      useEffect(() => {
        if (rectRef.current === null) {
          const svg = d3.select("#svg");
          calls += 1;
          console.log("Re-rendered: " + calls); // calls only one time
          //naming the selection
          rectRef.current = svg.append("rect");
          rectRef.current
            .attr("x", 0)
            .attr("y", 0)
            .attr("width", 300)
            .attr("height", 300)
            .attr("fill", "red")
            .attr("id", "red-rect");
        } else {
          rectRef.current.on("click", handleClick);
        }
      }, [handleClick]);
    
      return (
        <div>
          <svg id="svg" />
    
          <div>{counter}</div>
        </div>
      );
    }
    

    CodeSandbox


  2. Pass a function to setCounter. The parameter of that function is the previous value of the state.

    const handleClick = useCallback(() => {
        setCounter((prev) => prev + 1);
      }, []);
    

    useCallback is not necessary here though, since setCounter does not change identity. useReducer is a better hook for just incrementing as well.

      const [counter, incrementCounter] = useReducer((x) => x+1, 0);
    
      useEffect(() => {
        console.log("render"); // Renders once (or twice in strict mode)
        const svg = d3.select("#svg");
    
        //naming the selection
        const redRect = svg
          .append("rect")
          .attr("x", 0)
          .attr("y", 0)
          .attr("width", 300)
          .attr("height", 300)
          .attr("fill", "red")
          .attr("id", "red-rect");
    
        redRect.on("click", incrementCounter);
      }, [incrementCounter]); // dependency is not necessary, but doesn't hurt.
    

    Code Sandbox


    Snippet

    const rootElement = document.getElementById("root");
    const root = ReactDOM.createRoot(rootElement);
    
    root.render(<App />);
    
    let calls = 0;
    
    function App() {
      const [counter, incrementCounter] = React.useReducer((x) => x + 1, 0);
    
      React.useEffect(() => {
        calls += 1;
        console.log("Re-rendered: " + calls); // Renders once (or twice in strict mode)
        const svg = d3.select("#svg");
    
        //naming the selection
        const redRect = svg
          .append("rect")
          .attr("x", 0)
          .attr("y", 0)
          .attr("width", 300)
          .attr("height", 100)
          .attr("fill", "red")
          .attr("id", "red-rect");
    
        redRect.on("click", incrementCounter);
      }, [incrementCounter]); // dependency is not necessary, but doesn't hurt.
    
      return (
        <div>
          <svg id="svg" />
          <div>{counter}</div>
        </div>
      );
    }
    svg {
      height: 100px;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
    
    <div id="root"></div>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search