React: How to traverse through all children nodes in the tree view structure? - reactjs

I am working with react-expandable-treeview structure for displaying a family tree data. With the following code, I am traversing through the items object with family tree data, storing it into const data and then recursively passing into TreeView like it is indicated in documentation of react-expandable-treeview:
const items = props.item;
var x = 0;
const data = [{
id: x,
label: items.first_name, // parent #1
children: items.children.map( (child, idx) => {
id: x= ++idx,
label: child.first_name, // child of parent #1
children: child.children.map( (child_child) => ({
id: x = ++idx,
label: child_child.first_name,
children: child_child.children
? child_child.children.map( (child_child_child) => ({
id: x = ++idx,
label: child_child_child.first_name,
children: child_child_child.children
}))
: null
}))
}))
}]
However, with the algorithm above, I was able to only get the root node, its children, and two more generations of the family. This is a screenshot of how the structure looks like, it shows the amount of the nodes and their relationships correctly, but their indexes and first names are not displayed correctly.
I can't figure out how to perform the last mapping function, so that
id: x = ++idx,
label: the_rest_of_children.first_name,
children: the_rest_of_children.children
worked for the rest of children and expanded all nodes children until the very last node. How can I fix my code to get the correct output?

Use recursion
let index = 0
function visit(node) {
return {
label: node.first_name,
id: index++,
// pass `visit` function as an argument
children: node.children.map(visit)
}
}
const data = visit(props.items)
Recursion isn't the only way to traverse tree-like structures. If your tree is extremely complex, it'd be safer to implement iterative traversal algorithm:
If the recursive implementation is so simple, why bother with an iterative one? Of course, to avoid stack overflow. Most runtime engines/compilers set a limit on how many nested calls a program can make. If the height of the tree is larger than this limit, the program will crash with a stack overflow error. ...
But those cases aren't common in web development.

Related

Does a complex/deeply nested client-side state need to be normalised?

Background
I am building a single-page-application in React whose data will be retrieved from a relational database. Consider the following two tables:
menu
menu_items
A menu has many menu items. Menu items can be related to other menu items (represented in the database as an adjacency list). On the client, I'm representing it as a tree, i.e.:
{
"id": "menu",
"items": [
{
"id": "item-1",
"name": "Breakfast",
"children": []
},
{
"id": "item-2",
"name": "Lunch",
"children": [{ "id": "item-2-1", "children": [] }]
}
]
}
UI
A tree can get four levels deep and is typically much wider than it is tall. It is currently rendered recursively in the following way:
type Properties = {
items: {
id: string;
name: string;
children: Properties["items"];
}[];
};
const Items = ({ items }: Properties) => (
<ul>
{items.map((item) => (
<li key={item.id}>
{item.name}
<Items items={item.children} />
</li>
))}
</ul>
);
Problem
I have reached the stage where I want to update specific nodes in the tree. This operation seems complex, because it involves searching and replacing entire subtrees. Additionally, it will happen often, i.e. onChange, as a user updates item.name.
Although I don't use Redux, the following article explains it could be better to normalise nested client-side data to make operations like this easier: https://redux.js.org/usage/structuring-reducers/normalizing-state-shape.
Example
const menu = {
"id": "menu",
"itemMap": {
"item-1": { "parentId": null },
"item-2": { "parentId": null },
"item-2-1": { "parentId": "item-2" }
}
}
Question
Would I not have to denormalise/turn it back into a tree to render the UI? If yes, is there any point in my normalising the data?
I don't have a lot of experience with this and am struggling to find the right resources to answer the questions I have.
As with most engineering problems, there isn't a "correct" answer — rather it is a tradeoff: it depends on the expected use.
Your current approach optimizes for the maximum render performance at the cost of mutation performance. By using a tree structure, no transformation is needed at render time (just iteration) — however, arbitrary node lookups within the tree can't be done in constant time.
Another approach is to store the data as an associative array of nodes (Node ID ➡️ Node — e.g. Object/Map), which will optimize for arbitrary node lookup — and you can simply build the tree on every render by including each node's child IDs as part of its structure.
Here's an example of such a structure using the data that you provided:
TS Playground
<script src="https://cdn.jsdelivr.net/npm/#babel/standalone#7.20.15/babel.min.js"></script><script>Babel.registerPreset("tsx", {presets: [[Babel.availablePresets["typescript"], {allExtensions: true, isTSX: true}]]});</script>
<script type="text/babel" data-type="module" data-presets="tsx">
/** Any application-specific data type must satisfy this */
type ValidNode = {
/** Unique */
id: string;
/** Not allowed because of conflict */
children?: never;
};
// Your application-specific data type:
type Item = {
id: string;
name: string;
// etc.
};
type ListNode<T extends ValidNode> = T & {
/** List of child IDs */
children?: string[] | undefined;
};
// An object consisting of ID keys and ListNode values:
type ListNodeMap<T extends ValidNode> = Partial<Record<string, ListNode<T>>>;
const nodeMap: ListNodeMap<Item> = {
"menu": {
id: "menu",
name: "Menu",
children: ["item-1", "item-2"],
},
"item-1": {
id: "item-1",
name: "Breakfast",
},
"item-2": {
id: "item-2",
name: "Lunch",
children: ["item-2-1"],
},
"item-2-1": {
id: "item-2-1",
name: "Irresistibly healthy",
},
};
// A type-safe getter function which throws on bad IDs:
function getNode (
nodeMap: ListNodeMap<Item>,
id: string,
): ListNode<Item> {
const node = nodeMap[id];
if (!node) throw new Error(`Node ID ${JSON.stringify(id)} not found`);
return node;
}
/** The "linked" version of a ListNode */
type TreeNode<T extends ValidNode> = T & {
children?: T[] | undefined;
};
// Note: This uses recursion because it's for tree structures.
// Calling with list nodes having cyclic reference IDs will create an infinite loop.
function createTree (
nodeMap: ListNodeMap<Item>,
id: string,
): TreeNode<Item> {
const node = getNode(nodeMap, id);
return {
...node,
children: node.children?.map(id => createTree(nodeMap, id)),
};
}
console.log("node map:", nodeMap);
console.log("tree:", createTree(nodeMap, "menu"));
</script>
You didn't show how you receive the data, but if your API returns nodes with parent IDs instead of child IDs, then you can simply lookup each parent when acquiring new children and insert the child IDs at the time of acquisition — using an intermediate mapping structure if needed... that's tangential to the asked question.
You also didn't show how you plan to update node names, so I've excluded that part in the linked playground below, but here's an otherwise complete example of the code above with state and a reducer for updating an arbitrary node: Full example with state and reducer in TS Playground

Move list item to new parent without flickering

Imagine the following list:
Managing Director
Sales Director
IT Director
Technical Lead
Software Developer
Support Technician
HR Department
HR Officer
HR Assistant 1
HR Assistant 2
It's backed by a state in the form of:
[
{
id: 1,
text: "Managing Director",
children: [
{
id: 2,
text: "Sales Director"
}
...
]
}
...
]
Now I want to indent Support Technician. I would modify the state array to remove the item from the Technical Lead parent & add it to the Software Developer parent. The problem is, that React first deletes it, which causes all items below it to jump one line up, and then in the next frame adds it again to the new parent, which pushes those items a line down again. This appears as a flicker. It doesn't happen every time (sometimes react manages to render both in the same frame), but often enough it happens and is very distracting.
The state is modified in a way, that the parent passes its state callback setState down to its children. In this case, the initial state of the Technical Lead node looks like:
{
id: 4,
text: "Technical Lead",
children: [
{
id: 5,
text: "Software Developer"
},
{
id: 6,
text: "Support Technician"
}
]
}
As obvious from the state, every node renders all its children recursively.
After the indention, the state is modified to the following:
{
id: 4,
text: "Technical Lead",
children: [
{
id: 5,
text: "Software Developer",
chiilderen: [
{
id: 6,
text: "Support Technician"
}
]
}
]
}
If I were to this without React and instead with regular DOM APIs, I would move the node to the new parent with something like insertBefore(). React on the other hand unmounts & remounts the node.
Below is a simplified example of my "Node" component, which renders the list:
const Node = ({data, setSiblings}) => {
const [children, setChildren] = useState(data.children)
function indent() {
setSiblings(siblings => {
// const prevSibling = find the item in the state array
// const thisNode = {id, text, children}
const newPrevSibling = {...prevSibling, children: [thisNode]}
const siblingsWithout_ThisNode = deleteFromArray(siblings, thisNodeIndex)
// updateAtIndex() returns a new array with the modification (immutable)
return updateAtIndex(siblingsWithout_ThisNode, prevSiblingIndex, newPrevSibling)
})
}
const childNodes = children?.map(child =>
<Node data={child} setSiblings={setChildren} key={child.id}/>
)
return (
<li>
<div>{data.text}</div>
{childNodes ? <ul>{childNodes}</ul> : null}
</li>
)
}
The indent() function is triggered by a Tab press, but I didn't include the key handler logic here
I didn't find a solution to this problem directly, but I switched to using MST (MobX-State-Tree) for state management now and it worked with it (didn't flicker anymore - seemingly, both the unmounting & remounting of the component happen in the same frame now).
Below is a CodeSandbox with the MST implementation. When clicking e.g. on the Support Technician node, you can press Tab to indent and Shift + Tab to outdent (you have to click again, since it loses focus)
https://codesandbox.io/s/priceless-keldysh-17e9h?file=/src/App.js
While this doesn't answer my question directly, it helped solve the problem semantically (it's better than nothing).

Array to excel React

I am a little new to this, I am trying to pass my data array to an excel, but nothing works for me so far
I have an example
import {ExcelFile, ExcelSheet} from "react-export-excel";
const [listSelectNewTable, setListSelectNewTable] = useState([])
const DataExcel = [
{
columns: [
{ value: "Clave Cliente", widthPx: 50 },
{ value: "Nobre Cliente", widthCh: 20, widthCh: 20 },
{ value: "Clave Articulo", widthPx: 60},
{ value: "Nombre Cliente", widthPx: 60},
{ value: "Clave Unidad", widthPx: 60},
{ value: "Precio", widthPx: 60},
],
data: [listSelectNewTable],
}
];
class Download extends React.Component {
render() {
return (
<ExcelFile>
<ExcelSheet dataSet={DataExcel} name="Price"/>
</ExcelFile>
);
}
Can someone tell me what I'm doing wrong?
Before we begin:
Well obviously you're not going to have any data, if you're not passing any to the dataSet:
const [listSelectNewTable, setListSelectNewTable] = useState([])
// listSelectNewTable = []
Also you are attempting to call useState outside of a reactjs component and then proceed to use a class component later down the line. Without meaning to sound condescending, you really should study up on when to use life-cycle methods and when to use hooks (hint: it's either one or the other), because it's outside of the scope of this question / answer, but your code will never work if you don't put the time in to at least understand the basics.
The remainder of my answer assumes you at least fixed that.
The Problem:
Furthermore, even if it had any data, it would result in an error, because your data is incorrectly typed:
The dataSet prop type is defined as:
dataSet: Array<ExcelSheetData>
considering the following package type definitions:
interface ExcelSheetData {
xSteps?: number;
ySteps?: number;
columns: Array<string>;
data: Array<ExcelCellData>;
}
type ExcelCellData = ExcelValue | ExcelCell;
type ExcelValue = string | number | Date | boolean;
interface ExcelCell {
value: ExcelCell;
// [Note]: this is actually incorrectly typed in the package itself.
// Should be value: ExcelCellValue instead
style: ExcelStyle;
}
Essentially, if you had typescript enabled (which by the way I'd advise you to do), you'd see you're attempting to pass incorrect data-type to the dataSet property.
What you should be passing to dataSet prop is
Array<ExcellCellData>
// expecting: Array<ExcelValue | ExcelCell> (eg. ['A', 22])
What you are actually passing to to dataSet prop is
Array<Array<never>>
// received: [[]]
// [] = Array<never>
As you can see, the Array<Array<>> wraps it twice, meaning you are passing an unnecessarily wrapped array there.
Solution:
Now that we know the cause, let's fix it:
const DataExcel = [
{
columns: [
{ value: "Clave Cliente", widthPx: 50 },
{ value: "Nobre Cliente", widthCh: 20, widthCh: 20 },
{ value: "Clave Articulo", widthPx: 60},
{ value: "Nombre Cliente", widthPx: 60},
{ value: "Clave Unidad", widthPx: 60},
{ value: "Precio", widthPx: 60},
],
data: listSelectNewTable, // previously was [listSelectNewTable]
// Note we removed the extra wrapping nested array [] ˆ
},
];
Now you are passing your data correctly - but obviously you're not going to have anything inside the excel sheet barring the columns unless you pass any actual data instead of the empty array.
Final note (not necessary for answer):
Also in general I'd recommend using packages that have higher star ratings on their github page (this one has 65) and is only a fork of a an already existing package, especially for more complex stuff like excel exporting. While there sometimes truly are hidden diamonds in the rough, most of the time the stars are a good indicator of the quality of the package.
The fact that I found a typing error inside my own answer after a random glance at the package for the first time makes me fear how many other errors are actually in it, unless you know and trust the author or it's a package for a very niche thing.
Because the recursive
interface ExcelCell {
value: ExcelCell; // should be ExcelCellValue
style: ExcelStyle;
}
makes positively no sense and would just require you to recursively state the object interface definition ad infinitum
There are a couple of things that you need to change:
You cannot use the useState hook with a class component. To use this hook, change your component to a functional one.
You are not using the library correctly. Here is a good example of how to use it: https://github.com/rdcalle/react-export-excel/blob/master/examples/simple_excel_export_01.md
Specifically, you did not add any column to the sheet.
I'm guessing you want to provide dynamic data for the component (it should not always display the same stuff). If you want to do this, you should inject the data at the parent's component level.
So for your class:
import React, { useState } from 'react';
import ReactExport from "react-export-excel";
const ExcelFile = ReactExport.ExcelFile;
const ExcelSheet = ReactExport.ExcelFile.ExcelSheet;
const ExcelColumn = ReactExport.ExcelFile.ExcelColumn;
const Download = ({ data }) => {
const [listSelectNewTable, setListSelectNewTable] = useState(data);
return (
<ExcelFile>
<ExcelSheet data={listSelectNewTable} name="Price">
{
listSelectNewTable[0].columns.map(d =>
<ExcelColumn label={d.value} value={0} />)
}
</ExcelSheet>
</ExcelFile>
); }
export default Download;
And for the parent:
<Download data={DataExcel} />
With the data you had originally in your class in the parent, so that you can inject it into your component.

how to create tree from flat array using in Angular

I need to show a tree from a flat array of data using Angular and I'm open to use any package to render the tree. as a user, I should be able to click on a node and find out details such as node ID and title. the tree should be expanded on load but users should be able to collapse parent nodes as they wish. my node data model looks like below:
export class Node {
nodeID: number;
title: string;
parentNodeID: number;
}
and my data looks like this:
public Nodes: Node[] = [
{
nodeID: 1;
title: parent1;
parentNodeID: null;
},
{
nodeID: 2;
title: child1;
parentNodeID: 1;
},
{
nodeID: 3;
title: child2;
parentNodeID: 1;
}
]
you need a recursive algorithm that looks loops through your flat array and maps parentNodeID to Node to generate tree structure and then use a tree component, for example angular-tree-component, to render your tree.
I made a demo on stackblitz. have a look and let me know if it helped.
https://stackblitz.com/github/ramin-ahmadi/Flat-Tree
There is plenty of package that could do the job, for example This one. I did not tried it but seems easy to use. If you can, change your keys in your array. Else, then just map your items into another array, something like:
const newArray = array.map(item =>({
id: item.nodeID,
name: item.title,
children: array.filter(el => el.parentNodeID === parentId), // Not sure about that, but this is the idea
})
);
newArray wil be the data provided to your three.

Tell apollo-client what gets returned from X query with Y argiments?

I have a list of Items of whatever type. I can query all of them with query items or one with query item(id).
I realize apollo can't know what will be returned. It knows the type, but it doesn't know the exact data. Maybe there is a way not to make additional request? Map one query onto another?
Pseudo-code:
// somewhere in Menu.tsx (renders first)
let items = useQuery(GET_ITEMS);
return items.map(item => <MenuItemRepresenation item={item} />);
// meanwhile in apollo cache (de-normalized for readability):
{ ROOT_QUERY: {
items: [ // query name per schema
{ id: 1, data: {...}, __typename: "Item" },
{ id: 2, data: {...}, __typename: "Item" },
{ id: 3, data: {...}, __typename: "Item" },
]
}
}
// somewhere in MainView.tsx (renders afterwards)
let neededId = getNeededId(); // 2
let item = useQuery(GET_ITEM, { variables: { id: neededId } } );
return <MainViewRepresentation item={item} />;
Code like this will do two fetches. Even though the data is already in the cache. But it seems apollo thinks on query level. I would like a way to explain to it: "If I make item query, you need to look over here at items query you did before. If it has no item with that id go ahead and make the request."
Something akin to this can be done by querying items in MainView.tsx and combing through the results. It might work for pseudo-code, but in a real app it's not that simple: cache might be empty in some cases. Or not sufficient to satisfy required fields. Which means we have to load all items when we need just one.
Upon further research Apollo Link looks promising. It might be possible to intercept outgoing queries. Will investigate tomorrow.
Never mind apollo link. What I was looking for is called cacheRedirects.
It's an option for ApolloClient or Cache constructor.
cacheRedirects: {
Query: {
node: (_, args, { getCacheKey }) => {
const cacheKey = getCacheKey({
__typename: "Item",
id: args.id,
});
return cacheKey;
},
},
},
I'd link to documentation but it's never stable. I've seen too many dead links from questions such as this.

Resources