C.Programming
C.Programming

Reputation: 315

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:

Component to display recursive model

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.

Upvotes: 0

Views: 972

Answers (1)

brietsparks
brietsparks

Reputation: 5016

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.

Upvotes: 2

Related Questions