7

I'm trying to create a brush interaction on a timeline with D3 and React but I can't figure out why it doesn't work.

Everything seems fine but when I set a state within the useEffect() the index variable creates an infinite loop.

    const Timeline = () => {
    const svgRef = useRef(null!);
    const brushRef = useRef(null!);
    const x = scaleLinear().domain([0, 1000]).range([10, 810]);
    const [pos, setPos] = useState([]);

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

        svg.select('.x-axis')
            .attr('transform', `translate(0,${110})`)
            .call(axisBottom(x));

        const brush = brushX()
            .extent([
                [10, 10],
                [810, 110],
            ])
            .on('start brush end', function () {
                const nodeSelection = brushSelection(
                    select(brushRef.current).node(),
                );
                const index = nodeSelection.map(x.invert);

                setPos(index);
            });

        console.log(pos);

        select(brushRef.current).call(brush).call(brush.move, [0, 100].map(x));
    }, [pos]);

    return (
        <svg ref={svgRef} width="1200" height={800}>
            <rect x={x(200)} y={10} width={400} height={100} fill={'blue'} />
            <g className="x-axis" />
            <g ref={brushRef} />
            <text>{pos[0]}-{pos[1]}</text>
        </svg>
    );
};

Any ideas to fix that? Thanks!

user990463
  • 439
  • 1
  • 7
  • 27
  • If I see corectly, you pass `data` as props only to use it as dependency for the `useEffect`? And you set state of the position and not use it anywhere? There is a lot going on for no obvious reason. Please update the code or explain. – Croolman Aug 08 '22 at 07:52
  • Data and Position will be used afterward but for the moment, setting a state of index variable causes an infinite loop. I'll remove Data for clarity. – user990463 Aug 08 '22 at 08:14
  • React use effect with asynchronous task has specific interactions, have you try to wrap in a function and call it like an ajax request ? – Charlie Lucas Aug 08 '22 at 08:18
  • @user990463 Now this for sure triggers an infinite loop. One of the rules of `useEffect` do not put into its dependency something you change inside the effect. What you're saying is "If A changes do B where in B I update A" .. And you can read that sentence forever. – Croolman Aug 08 '22 at 08:24
  • Yes, I tried to wrap the brush function into a useCallback() then set the state inside but without luck. – user990463 Aug 08 '22 at 08:35
  • For the record, If I set a string in the state like setPos('test') instead of `index`, it breaks the infinite loop. – user990463 Aug 08 '22 at 08:54
  • @user990463 ofc it does, because the `useEffect` compares the value in dependency array - if it has changed it gets triggered if not, it does not get triggered (in this case constant string is still the same). Please inspect the basics behind `useEffect` hook (when and how it triggers) and how/when React triggers re-renders – Croolman Aug 09 '22 at 06:03

1 Answers1

2

Seems that this solves the problem:

const runBrushes = useCallback(async () => {
    const brush = brushX()
        .extent([
            [10, 10],
            [810, 110],
        ])
        .on('start brush end', function () {
            const nodeSelection = brushSelection(
                select(brushRef.current).node(),
            );
            const index = nodeSelection.map(x.invert);

            setPos(index);
        });

    select(brushRef.current).call(brush).call(brush.move, [0, 100].map(x));
}, [pos, x]);

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

    svg.select('.x-axis')
        .attr('transform', `translate(0,${110})`)
        .call(axisBottom(x));

    runBrushes().catch(console.error);
}, []);
user990463
  • 439
  • 1
  • 7
  • 27