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

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

Related

How do I select and update an object from a larger group of objects in Recoil?

My situation is the following:
I have an array of game objects stored as an atom, each game in the array is of the same type and structure.
I have another atom which allows me to store the id of a game in the array that has been "targeted".
I have a selector which I can use to get the targeted game object by searching the array for a match between the game ids and the targeted game id I have stored.
Elsewhere in the application the game is rendered as a DOM element and calculations are made which I want to use to update the data in the game object in the global state.
It's this last step that's throwing me off. Should my selector be writable so I can update the game object? How do I do this?
This is a rough outline of the code I have:
export const gamesAtom = atom<GameData[]>({
key: 'games',
default: [
{
id: 1,
name: 'Bingo',
difficulty: 'easy',
},
{
id: 21,
name: 'Yahtzee',
difficulty: 'moderate',
},
{
id: 3,
name: 'Twister',
difficulty: 'hard',
},
],
});
export const targetGameIdAtom = atom<number | null>({
key: 'targetGameId',
default: null,
});
export const targetGameSelector = selector<GameData | undefined>({
key: 'targetGame',
get: ({ get }) => {
return get(gamesAtom).find(
(game: GameData) => game.id === get(selectedGameIdAtom)
);
},
// This is where I'm getting tripped up. Is this the place to do this? What would I write in here?
set: ({ set, get }, newValue) => {},
});
// Elsewhere in the application the data for the targetGame is pulled down and new values are provided for it. For example, perhaps I want to change the difficulty of Twister to "extreme" by sending up the newValue of {...targetGame, difficulty: 'extreme'}
Any help or being pointed in the right direction will be appreciated. Thanks!

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).

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.

How to model recursively nested data in state

I have a data structure typed like:
export interface IGroup {
id: number;
name: string;
groupTypeId: number;
items: IItem[];
groups: IGroup[];
}
Which recursively represents many to many relationships between a "Group" and a "Group" and an "Group" and an "Item". Groups are made up of items and child groups. An item derives to just a simple type and other meta data, but can have no children. A single group represents the top of the hierarchy.
I currently have components, hooks, etc to recursively take a single group and create an edit/create form as shown below:
I have this form "working" with test data to produce a standard data output as below on save:
{
"1-1": {
"name": "ParentGroup",
"groupType": 2
},
"2-4": {
"name": "ChildGroup1",
"groupType": 1
},
"2-9": {
"name": "ChildGroup2",
"groupType": 3
},
"2-1": {
"itemType": "FreeForm",
"selectedName": "Testing",
"selectedClass": 5
},
"2-2": {
"itemType": "FreeForm",
"selectedName": "DisplayTest",
"selectedClass": 5
},
"3-4": {
"itemType": "EnumValue",
"selectedItem": {
"id": 12900503,
"name": "TRUE"
}
},
"3-5": {
"itemType": "EnumValue",
"selectedItem": {
"id": 12900502,
"name": "FALSE"
}
},
"3-9": {
"itemType": "FreeForm",
"selectedName": "Test",
"selectedClass": 5
},
"3-10": {
"itemType": "FreeForm",
"selectedName": "Tester",
"selectedClass": 5
},
"3-11": {
"itemType": "FreeForm",
"selectedName": "TestTest",
"selectedClass": 5
}
}
The "key" to these objects are the grid column and row since there are no other guaranteed unique identifiers (if the user is editing, then it is expected groups have ids in the db, but not if the user is adding new groups in the form. Otherwise, the name is an input form that can be changed.) It makes sense and it is easy to model the keys this way. If another group or item is added to the hierarchy, it can be added with its column and row.
The problem that I have is that I would love to be able to have an add button that would add to a groups items or group arrays so that new rows in the hierarchy could be created. My forms should handle these new entries.
Ex.
"1-1": {
groups: [..., {}],
items: [..., {}]
}
But the only data structure that I have is the IGroup that is deeply nested. This is not good for using as state and to add to this deeply nested state.
The other problem I have is that I need to be able to map the items and groups to their position so that I can translate to the respective db many to many tables and insert new groups/items.
Proposed solution:
I was thinking that instead of taking a group into my recursive components, I could instead create normalized objects to use to store state. I would have one object keyed by column-row which would hold all the groups. Another keyed by column-row to hold all the items. Then I think I would need two more objects to hold many to many relationships like Group to Group and Group to Item.
After I get the data from the form, I hopefully can loop through these state objects, find the hierarchy that way and post the necessary data to the db.
I see that this is a lot of data structures to hold this data and I wasn't sure if this was the best way to accomplish this given my modeling structure. I have just started using Redux Toolkit as well, so I am somewhat familiar with reducers, but not enough to see how I could apply them here to help me. I have been really trying to figure this out, any help or guidance to make this easier would be much appreciated.
Go with normalizing. Each entity having a single source of truth makes it much easier to read and write state.
To do this, try normalized-reducer. It's a simple higher-order-reducer with a low learning curve.
Here is a working CodeSandbox example of it implementing a group/item composite tree very similar to your problem.
Basically, you would define the schema of your tree:
const schema = {
group: {
parentGroupId: { type: 'group', cardinality: 'one', reciprocal: 'childGroupIds' },
childGroupIds: { type: 'group', cardinality: 'many', reciprocal: 'parentGroupId' },
itemIds: { type: 'item', cardinality: 'many', reciprocal: 'groupId' }
},
item: {
groupId: { type: 'group', cardinality: 'one', reciprocal: 'itemIds' }
}
};
Then pass it into the library's top-level function:
import normalizedSlice from 'normalized-reducer';
export const {
emptyState,
actionCreators,
reducer,
selectors,
actionTypes,
} = normalizedSlice(schema);
Then wire up the reducer into your app (works with both React useReducer and the Redux store reducers), and use the selectors and actionCreators to read and write state.

How can I get an item in the redux store by a key?

Suppose I have a reducer defined which returns an array of objects which contain keys like an id or something. What is the a redux way of getting /finding a certain object with a certain id in the array. The array itself can contain several arrays:
{ items:[id:1,...],cases:{...}}
What is the redux way to go to find a record/ node by id?
The perfect redux way to store such a data would be to store them byId and allIds in an object in reducer.
In your case it would be:
{
items: {
byId : {
item1: {
id : 'item1',
details: {}
},
item2: {
id : 'item2',
details: {}
}
},
allIds: [ 'item1', 'item2' ],
},
cases: {
byId : {
case1: {
id : 'case1',
details: {}
},
case2: {
id : 'case2',
details: {}
}
},
allIds: [ 'case1', 'case2' ],
},
}
Ref: http://redux.js.org/docs/recipes/reducers/NormalizingStateShape.html
This helps in keeping state normalized for both maintaining as well as using data.
This way makes it easier for iterating through all the array and render it or if we need to get any object just by it's id, then it'll be an O(1) operation, instead of iterating every time in complete array.
I'd use a library like lodash:
var fred = _.find(users, function(user) { return user.id === 1001; });
fiddle
It might be worth noting that it is seen as good practice to 'prefer objects over arrays' in the store (especially for large state trees); in this case you'd store your items in an object with (say) id as the key:
{
'1000': { name: 'apple', price: 10 },
'1001': { name: 'banana', price: 40 },
'1002': { name: 'pear', price: 50 },
}
This makes selection easier, however you have to arrange the shape of the state when loading.
there is no special way of doing this with redux. This is a plain JS task. I suppose you use react as well:
function mapStoreToProps(store) {
function findMyInterestingThingy(result, key) {
// assign anything you want to result
return result;
}
return {
myInterestingThingy: Object.keys(store).reduce(findMyInterestingThingy, {})
// you dont really need to use reduce. you can have any logic you want
};
}
export default connect(mapStoreToProps)(MyComponent)
regards

Resources