I have a functional ponent
with a d3
function which appends a p
to the ref
on mount
Im trying to change the text content of the appended paragraph using the useState
hook
clicking the button
<button type="button" onClick={(e)=> setData("bar")}>change data</button>
triggers a re-render, instead of changing the text of the p
tag , a new version of the d3 function gets created --> appends a new p
reading How to rerender when refs change suggest to use the useCallback
hook
which im not quit sure how implement
how to achive a a re-render of the paragraph without creating a new one?
import {useEffect, useState, useRef} from 'react'
import * as d3 from 'd3'
function DomReference() {
const domRef = useRef()
const [data, setData] = useState("foo")
useEffect(()=>{
d3.select(domRef.current)
.append('p')
.text(data)
// re-render --> creates a new function
}, [data])
return (
<div ref={domRef}>
<button type="button" onClick={(e)=> setData("bar")}>change data</button>
</div>
)
}
export default DomReference
I have a functional ponent
with a d3
function which appends a p
to the ref
on mount
Im trying to change the text content of the appended paragraph using the useState
hook
clicking the button
<button type="button" onClick={(e)=> setData("bar")}>change data</button>
triggers a re-render, instead of changing the text of the p
tag , a new version of the d3 function gets created --> appends a new p
reading How to rerender when refs change suggest to use the useCallback
hook
which im not quit sure how implement
how to achive a a re-render of the paragraph without creating a new one?
import {useEffect, useState, useRef} from 'react'
import * as d3 from 'd3'
function DomReference() {
const domRef = useRef()
const [data, setData] = useState("foo")
useEffect(()=>{
d3.select(domRef.current)
.append('p')
.text(data)
// re-render --> creates a new function
}, [data])
return (
<div ref={domRef}>
<button type="button" onClick={(e)=> setData("bar")}>change data</button>
</div>
)
}
export default DomReference
Share
Improve this question
asked Jun 23, 2021 at 12:26
SatorSator
7765 gold badges17 silver badges40 bronze badges
0
2 Answers
Reset to default 3 +50The d3 append function will always create a new element whenever it is executed.
To have always one paragraph and only update its value, you can use d3.join
.
instead of:
d3.select(domRef.current)
.append('p')
.text(data)
You could use:
d3.select(domRef.current).selectAll('p')
.data([data])
.join('p')
.text(d => d)
Explaining line by line:
d3.select(domRef.current).selectAll('p')
: Selects all the<p>
elements indomRef.current
.data([data])
: binds the<p>
elements to an array of data. When you bind, each item in the array will have its own<p>
..join('p')
: Thejoin
will create a selection that removes all the<p>
that do not have data associated with, and will update the ones that have data. This will leave you with one<p>
, since the.data()
call before only had one element in the array.text(d => d)
: This callback is how you access the data that is associated with the<p>
.
As a plement:
if you don't want to remove all the other <p>
, you can use a class to narrow your selection:
d3.select(domRef.current).selectAll('p.myOddParagraphs')
.data([1, 3, 5])
.join('p')
.classed('myOddParagraphs', true)
.text(d => d)
d3.select(domRef.current).selectAll('p.myEvenParagraphs')
.data([2, 4, 6])
.join('p')
.classed('myEvenParagraphs', true)
.text(d => d)
The code above would result in 6 paragraphs: three are bound to the [1, 3, 5]
array, and the other three are bound to [2, 4, 6]
In principle, you shouldn't attempt to mutate DOM using D3 while using React, because React would be unaware of such changes. This usually leads to very nasty bugs that are difficult to spot and debug. If you find yourself using attr
or append
in React, you're using it wrong ;)
This is not to say you can't use D3 with React though. The correct way is to always leave DOM manipulations to React, and use D3 as engine that putes your properties. E.g. when using d3 force layout, you can pute (x,y) coordinates using D3, and pass them as props, so that React handles reconciliation etc.
In the above example, I'd suggest leaving the rendering of <p .../>
to React. I don't know however what is your end goal so can't really tell what's the correct way, apart from converting this example trivially to React