I've started introducing some React hooks into my code, specifically the useEffect
and I can't seem to find out whether what I'm doing is considered safe or not. Essentially I'm running animations on the DOM within the hook, and I want to ensure that's not going to break any DOM snapshots for example.
Here's an example, I've modified from my full example to try and be concise to illustrate what's happening:
export function GrowingCircle(props) {
const root = useRef(null); // This is the root element we draw to
// The actual rendering is done whenever the data changes
useEffect(() => {
const radius = props.width / 2;
d3.select(root.current)
.transition()
.duration(1000)
.attr("r", radius);
}, [props.width]);
return (
<svg width={props.width} height="100%">
<circle ref={root} cx="0" cy="0" r="0" fill="red" />
</svg>
);
}
The part I'm concerned about is the .transition()
is going to run frequent updates on the DOM for 1 second, and I'm unsure if that's going to mess up the react rendering?
A follow up question (as often we don't have control of the animation rendering like in this example). Would the following where the circle
is no longer within the JSX change things?
export function GrowingCircle(props) {
const root = useRef(null); // This is the root element we draw to
// The actual rendering is done whenever the data changes
useEffect(() => {
const radius = props.width / 2;
d3.select(root.current)
.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("fill", "red")
.transition()
.duration(1000)
.attr("r", radius);
}, [props.width]);
return (
<svg ref={root} width={props.width} height="100%">
</svg>
);
}
useEffect() is for side-effects. A functional React component uses props and/or state to calculate the output. If the functional component makes calculations that don't target the output value, then these calculations are named side-effects.
useEffect runs asynchronously after a render is painted to the screen, unblocking the browser paint process. So that looks like: We cause a render somehow (through the state, or the parent re-renders or context change).
The useState hook is used for storing variables that are part of your application's state and will change as the user interacts with your website. The useEffect hook allows components to react to lifecycle events such as mounting to the DOM, re-rendering, and unmounting.
The useLayoutEffect hook works synchronously. It runs immediately after React has performed all DOM mutations. It will run after every render but before the screen is updated.
DOM elements rendered by React can be modified. However if React needs to re-render because the Virtual DOM doesn't match with the current DOM (the one that React has as the current) it might replace the modified parts, and changes made out of React could be lost.
React modifies the necessary parts so modifications might remain if React only modified a part of the element or they might be removed. So, as far as I understand, we can not trust modifications will remain. Only if we knew for sure the Component output will not change after the custom modifications.
My suggestion is to use React to keep track of any change:
useState
and useEffect
to modify style
properties.I have been in your same scenario, to give you a short answer, since there is no real D3 - React implementation, you need to draw your own boundaries of imperative versus declarative rendering. However in both your example, you aren't really breaking any rules.
In your second example you're passing the reins to D3 to do the full rendering, while React simple keeps a ref to the top-level svg element. While in your first example, you're rendering a single circle, and only managing its transition with D3.
However in the 1st example, since it looks like you never change anything in the DOM after declaration, React should geneally never interfere. Here's a third example achieving something similar to what you wrote :
export function GrowingCircle(props) {
const root = useRef(null); // This is the root element we draw to
const [radius,setRadius] = useState(0); //initial radius value
useEffect(() => {
d3.select(root.current)
.transition()
.duration(1000)
.attr("r", props.width / 2)
.on("end", () => {setRadius(props.width / 2)}); //this is necessary
}, [props.width]);
return (
<svg width={props.width} height="100%">
<circle ref={root} cx="0" cy="0" r={radius} fill="red" />
</svg>
);
}
I've ran into a similar problem myself, and from what I understood, during the transition d3 acts on your desired value directly, causing it not to trigger react's re-render mechanism. But once it's done, I'm assuming it directly tries to act on the radius value, and your circle snaps back to its 0 original value. So you simply add an .on('end') event to update its state, and there you have it!
I feel like this is as close as it gets to the react way, where d3 only selects elements rendered based on react state.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With