Open McJezuss opened 3 years ago
Update: When I wrap it in a container, it clearly shows that it is indeed updating, but it is also rerendering. Is there a way to stop the rerender?
If anyone else is facing this issue, I found a workaround by doing the following:
const TreeWrapper = () => { return ( <Tree/> // Add your props linked to the state in here ) }
Hi @LunarAI, thanks for making me aware of this issue in v2.
I've created a small repro attempt with a (simplified) data fetch here (codesandbox.io). By default the tree seems to append data as expected, so it's not clear to me what exactly you're encountering in your case.
Could you fork the sandbox and configure it in a way that reproduces the issue? If you can't reproduce it there, please post an excerpt of your Tree component and which TreeProps/configuration you're using.
I'm aware that this is generally a tricky use case (#201; saw your additional comment there too), so I'm keen to do some bug squashing and adding explicit support for incremental data fetching.
Hi @bkrem, I am so sorry for not getting back to you yet. I will try to replicate it and submit the link here. It will probably be in early February.
No need to apologise 😄 Thanks again for bringing my attention (back) to this, since it's a use case that should get first-class support instead of requiring workarounds due to re-render quirks.
I think I realised what you were encountering after re-reading your explanations and playing around with the codesandbox above some more:
Without setting any initialDepth
this approach works (as you mentioned in your workaround, thank you for that too 👌 ).
But there's definitely unintended behaviour when using initialDepth
together with this incremental fetching approach due to re-rendering, i.e. the tree becomes "frozen".
I'm looking into a fix for the issue at the moment 🤞
Hi @bkrem. We have a similar issue where we are filtering a large amount of data, so need to add the data incrementally. I have replicated your codesandbox and have it working, but as you mention we need to set the initialDepth because the tree freezes when removed. Do you have any updates on if you are any closer to fixing the issue? I realise your last comment was just 5 weeks ago. Thanks.
Hi @stevepetebruce,
February was a super busy month for me both in- and outside of work, but I had some time this weekend to look more deeply into this topic and make some progress.
I want to clarify that this is really a whole new feature set for the library (explained below), rather than a bug, since there's currently no explicit support for this advanced use case.
TL;DR: react-d3-tree
does not currently differentiate between the concepts of two distinct data
sets vs one augmented/modified data
set. I'm currently figuring out how to allow for this differentiation in a meaningful way without adding boatloads of complexity.
The key issue is the following:
If
data
changes, how doesTree
know whether the new value is an entirely different dataset (A -> B) or is a mutation of the previous dataset (A -> A*)?
This differentiation can be achieved by adding a new dataKey: string |Â number
prop:
data
changed, but dataKey
is unchanged? It's A -> A*
data
and dataKey
both changed? It's A -> B
This approach works, but I'm still working on making the tree's behaviour align with common sense for the A -> A*
scenario:
Tree
could forget about all node configurations and reconfigure based on initialDepth
(or simply render to full depth) whenever data
changed.A -> A*
scenario introduces additional statefulness, as existing nodes should preserve their collapsed/expanded state between data
changes.Any solution for this? I am facing a similar challenge, re-rendering the entire tree when tree size gets too big doesn't look like a good option. Any suggestion on how to handle updates in a large tree? Thanks.
I saw the recent merge of https://github.com/bkrem/react-d3-tree/pull/417/files by @bkrem last month. Thanks. It looks like what I want but I've had no luck. Is there a test or example of using dataKey?
If I specify a dataKey (an id from my root node) on initial render then nothing renders. If I useState to set a datakey after my initial render of data that's fine but changes to data still re-render the tree (expanding all collapsed nodes). Seems like maybe the dataKey test in getDerivedStateFromProps should/could look to see it there is no prevState data and go ahead and render if not. Might save some hassle.
I was adding the children to my data manually so then I thought maybe I could/had to use the addChildren function. I called that from within my renderCustomNodeElement function but it doesn't add any children. I can see the children being pushed onto targetNodeDatum in handleAddChildrenToNode but they don't render and aren't on the nodeDatum in subsequent calls to renderCustomNodeElement. Probably a mistake in how I'm implementing this.
I tried modifying the codesandbox example above but couldn't get a version above 3.5.1 there.
Any updates? I faced the same issue
It's been a long time since I looked at this @ajzyn . I got it working but I forget what fixed it. Here's my code. If this helps you then maybe you could post the relevant portion.
import { FunctionComponent, useEffect, useState, useCallback } from "react";
import { Link } from "react-router-dom";
import Tree, {TreeProps, RawNodeDatum, CustomNodeElementProps, TreeNodeDatum} from "react-d3-tree";
import axios from "axios";
import { Card, Button, Modal, Header, Icon, Dimmer, Loader } from "semantic-ui-react";
import { get } from 'lodash';
const fetchTreeData = async (nodeId: string) => {
let { data } = await axios.get(`/api/myRoute/${nodeId}/treeData?depth=3`);
return data;
}
interface iProps {
rootNodeId: string,
orientation?: string
};
interface iCustomNodeProps extends CustomNodeElementProps {
foreignObjectProps: object;
}
const ScaleTree: FunctionComponent<iProps> = ({rootNodeId, orientation}) => {
const [ loading, setLoading ] = useState(false);
const [ dataKey, setDataKey ] = useState("");
const [ detailNodesMap, setDetailNodesMap ] = useState(new Map());
const [ errorFetching, setErrorFetching ] = useState(false);
const [ dimensions, setDimensions ] = useState({width: 1000, height: 1000});
const [ treeData, setTreeData ] = useState({} as RawNodeDatum);
// Fetches the specified node and its children from the API to a depth of 3 and
// returns the result
const fetchNode = useCallback(async (nodeId: string) => {
setLoading(true);
setErrorFetching(false);
let data = {} as RawNodeDatum;
try {
data = await fetchTreeData(nodeId);
}
catch (error) {
setErrorFetching(true);
console.log('error', error);
}
finally {
setLoading(false);
return data;
}
}, []);
const getInitialTreeData = useCallback(async (nodeId: string) => {
const data = await fetchNode(nodeId);
setTreeData(data);
setDataKey(nodeId);
}, [fetchNode]);
useEffect(() => {
const dims = document.getElementById("treeWrapper")?.getBoundingClientRect();
setDimensions({
width: dims?.width || 1000,
height: dims?.height || 1000
});
getInitialTreeData(rootNodeId);
}, [rootNodeId, getInitialTreeData])
const handleDetailsToggle = (nodeId: string) => {
const detailNodes = new Map(detailNodesMap);
detailNodes.set(nodeId, detailNodes.get(nodeId) ? false : true);
setDetailNodesMap(detailNodes);
}
const handleNodeToggle = async (nodeDatum: TreeNodeDatum, toggleNode: () => void, addChildren: any) => {
const nodeId: string = get(nodeDatum, 'attributes.id', 'n/a') as string;
const needToFetch: boolean = get(nodeDatum, 'children.length', 0) < get(nodeDatum, 'attributes.childCount', 0);
if(nodeId && needToFetch && nodeId !== 'n/a') {
const node = await fetchNode(nodeId);
if(get(node, "children.length", 0) > 0) {
addChildren(node.children);
}
if (nodeDatum.__rd3t.collapsed) {
toggleNode();
}
}
else {
toggleNode();
}
}
const renderForeignObjectNode = ({
nodeDatum,
toggleNode,
addChildren,
foreignObjectProps
}: iCustomNodeProps) => {
const nodeId = nodeDatum.attributes?.id;
let circleColor = nodeDatum.children && nodeDatum.__rd3t.collapsed ? "darkGray" : "white";
if(nodeDatum.attributes?.isTerminal) { circleColor = "blue"; }
if(nodeDatum.attributes?.isLeaf) { circleColor = "green"; }
if(nodeDatum.attributes?.isMissing) { circleColor = "red"; }
let childCount = nodeDatum.attributes?.childCount || "";
let details = "";
if(nodeDatum.attributes?.nodeTitle) {
details += `${nodeDatum.attributes?.nodeTitle}<br /><br />`;
}
if(nodeDatum.attributes?.calcField) {
details += `calcField: ${nodeDatum.attributes?.calcField}<br />`;
}
if(nodeDatum.attributes?.compValue) {
details += `compValue: ${nodeDatum.attributes?.compValue}<br />`;
}
if(nodeDatum.attributes?.WGAScale) {
const wgaScales = JSON.parse(nodeDatum.attributes?.WGAScale as string);
for(const wgaScale of wgaScales) {
details += `WGAScale: ${new Date(wgaScale.effectiveDate).getFullYear()}: $${wgaScale.value.toLocaleString('en-US', {maximumFractionDigits: 0})}<br />`;
}
}
if(nodeId && nodeDatum.attributes?.isLeaf && detailNodesMap.get(nodeId) === undefined) {
// show leaf node details by default first time through
const detailNodes = new Map(detailNodesMap);
detailNodes.set(nodeId, true);
setDetailNodesMap(detailNodes);
}
return (
<g>
<circle r={20} onClick={handleNodeToggle.bind(this, nodeDatum, toggleNode, addChildren)} fill={circleColor} />
<text onClick={handleNodeToggle.bind(this, nodeDatum, toggleNode, addChildren)} textAnchor="middle" stroke="black" strokeWidth="2px" dy=".3em">{childCount}</text>
{/* `foreignObject` requires width & height to be explicitly set. */}
<foreignObject {...foreignObjectProps} >
<div style={{textAlign: "center", padding: "10px 50px 20px 0"}}>
<Card style={{backgroundColor: "#f9f9f9"}}>
<Card.Content>
<Card.Header>{nodeDatum.name}</Card.Header>
{!nodeDatum.attributes?.isLeaf && (
<Card.Meta>{nodeDatum.attributes?.nodeText}</Card.Meta>
)}
{(nodeId && detailNodesMap.get(nodeDatum.attributes!.id as string)) && (
<Card.Description dangerouslySetInnerHTML={{__html: details}}></Card.Description>
)}
</Card.Content>
{nodeId && (
<Card.Content extra>
<div className='ui three buttons'>
<Button secondary content='Details' onClick={() => handleDetailsToggle(nodeId as string)} />
<Button primary content='Edit' as={Link} to={`/node/${nodeId}`} />
<Button content='Copy' onClick={() => navigator.clipboard.writeText(nodeId as string)} />
</div>
</Card.Content>
)}
</Card>
</div>
</foreignObject>
</g>
);
}
const nodeSize = { x: 450, y: 350 };
const separation = { siblings: 1, nonSiblings: 1 };
const foreignObjectProps = {
width: nodeSize.x,
height: nodeSize.y,
x: orientation === "vertical" ? -150 : 25,
y: orientation === "vertical" ? 15 : -55
};
return (
<div
style={{width: "100%", height: "100%"}}
>
<Dimmer active={loading}>
{loading && <Loader>Fetching Node...</Loader>}
</Dimmer>
<Tree
data={treeData}
orientation={(orientation || 'vertical') as TreeProps["orientation"]}
zoom={0.5}
nodeSize={nodeSize}
dimensions={dimensions}
translate={{ x: dimensions.width / 2, y: dimensions.height / 2 }}
separation={separation}
renderCustomNodeElement={(rd3tProps) =>
renderForeignObjectNode({ ...rd3tProps, foreignObjectProps })
}
dataKey={dataKey}
/>
<Modal
basic
onClose={() => setErrorFetching(false)}
open={errorFetching}
size='small'
>
<Header icon>
<Icon name='warning circle' />
</Header>
<Modal.Content>
<p>Error fetching Node</p>
</Modal.Content>
<Modal.Actions>
<Button basic color='red' inverted onClick={() => setErrorFetching(false)}>
<Icon name='remove' /> OK
</Button>
</Modal.Actions>
</Modal>
</div>
);
}
export default ScaleTree;
The code above worked for me. I have been searching for a solution to add nodes dynamically for a while now. The takeaway is you need to need to keep the DataKey constant and use the addChildren function directly on the node.
Thank you for taking the time to report an issue with react-d3-tree!
Feel free to delete any questions that do not apply.
Are you reporting a bug, or opening a feature request?
Bug
What is the actual behavior/output?
When updating the data dynamically with a state, the tree data doesn't actually update. The data I set at creation is the only data that it displays, regardless if I update it in the state or not.
What is the behavior/output you expect?
I expect the internal data of the tree to update.
Can you consistently reproduce the issue/create a reproduction case (e.g. on https://codesandbox.io)?
I would need access to our system, so no, but I am willing to share whatever you need to see.
What version of react-d3-tree are you using?
2.0.0