Question: How can I create a D3 transition animation to update a React component without re-rendering the whole plot?

I have a D3js Plot wrapped inside a React component (v7). For example a Bar Plot with a data table and a parameter for which column to plot. On change of the plotting variable, I do not want to re-render the whole plot but instead execute a D3 transition animation to the new variable.
Right now I have tried it following the stump-code here, but first I have problems getting the initial plot to render and second I really would like to understand what the correct React hook way is to achieve this…
import * as React from "react";
import * as d3 from "d3";

export function BarPlot({
  data,
  x,
  width,
  height,
}: {
  data: DataTable;
  x: string;
  width: number;
  height: number;
}) {
  const svgRef = React.useRef<SVGSVGElement>(null);
  const svg = d3.select(svgRef.current);

  const prevX = React.useRef<string>(x);
  
  
  if (svgRef.current === null) {
    svg.selectAll("*").remove();
    svg
      .append("g")
      .selectAll()
      .data(data)
    // …
    // Normal d3 plotting code here
    // …
  }


  if (prevX.current !== x) {
    // Update the plot, animate the transition from plotting the old bars to the new bars

    prevX.current = x;
  }

  return <svg ref={svgRef} width={width} height={height} />;
}

When looking around for the correct React-way to do this, it seems useEffect is not the right choice here. I also tried to use useMemo to save the inital plot, but even then I need to manually check whether the transitionable parameters have changed…
Abstract, I think the question is how to have a React component, where part of the render code is executed intially and another part only if the already rendered component has a change in one of the props.


Answer:
Here is an example of animated bar chart using React with D3.
Just add a useEffect on the SVG element ref and build the chart when ref is valid (when the component is mounted)


const MAX_VALUE = 200;

const BarChart = ({ data, height, width }) => {
  const svgRef = React.useRef(null);

  React.useEffect(() => {
    const svg = d3.select(svgRef.current);

    const xScale = d3.scaleBand()
      .domain(data.map((value, index) => index.toString()))
      .range([0, width])
      .padding(0.1);

    const yScale = d3.scaleLinear()
      .domain([0, MAX_VALUE])
      .range([height, 0]);

    const xAxis = d3.axisBottom(xScale)
      .ticks(data.length)
      .tickFormat((_, index) => data[index].label);

    svg
      .select("#x-axis")
      .style("transform", `translateY(${height}px)`)
      .style("font-size", '16px')
      .call(xAxis);

    const yAxis = d3.axisLeft(yScale);
    svg
      .select("#y-axis")
      .style("font-size", '16px')
      .call(yAxis)

    svg.selectAll('g.tick');

    const bars = svg
      .selectAll(".bar")
      .data(data)
      .join("g")
      .classed("bar", true);
      
    bars.append("rect")
      .style("transform", "scale(1, -1)")
      .attr("x", (_, index) => xScale(index.toString()))
      .attr("y", -height)
      .attr("width", xScale.bandwidth())
      .transition()
      .delay((_, index) => index * 500)
      .duration(1000)
      .attr("fill", d => d.color)
      .attr("height", (d) => height - yScale(d.value));
      
  }, [data]);

  return (
      <svg ref={svgRef} height={height} width={width} />
  );
};

const data = [
  {value: 50, color: '#008'}, 
  {value: 100, color: '#00C'}, 
  {value: 150, color: '#00f'}
];

ReactDOM.render(
  <BarChart data={data} width={300} height={170} />, 
  document.getElementById("chart")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.4/d3.min.js"></script>

<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>

<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id='chart'></div>