How to model recursively nested data in state - reactjs

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.

Related

Derive locally cached Apollo client state based off query/cached object updates

I have a query that retrieves a Model. Inside this model, there are nested models with fields.
The shape is roughly like this:
{
model: [
{
id: 1,
fields: [...]
},
{
id: 2,
fields: [...]
}
]
}
Additionally, the frontend needs the model normalized into a list, like this:
{
modelFields: [
{...},
{...},
{...},
{...}
]
}
I’m attempting to derive modelFields declaratively when a query or cache update changes model. I’m trying to achieve this in type-policies section on Model: { merge: modelMergeMiddleware }, like so:
export function modelMergeMiddleware(
__: ModelFragment,
incoming: ModelFragment,
{cache, readField}: FieldFunctionOptions
) {
if (incoming) {
cache.writeQuery({
query: ModelFieldsDocument,
data: {
modelFields: incoming.fieldsets.reduce(
(fields: ModelFieldFragment[], fieldset: FieldsetFragment) => {
return fields.concat(newFields)
},
[]
)
}
})
}
return incoming
}
However, this runs into problems:
nested cache references don’t get passed through leaving empty data
readField and lodash’s _.cloneDeep both result in Readonly data that cause errors
My question is two-fold:
Is there a method to work around the problems mentioned above to derive data in a merge function?
Is there a different approach where I can declaratively derive local-only state and keep it synchronized with cached objects?
Per question 2, my backup approach is to use a reactiveVar/Recoil to store this data. This approach has the tradeoff of needing to call a setter function in all the places the Model object gets queried or mutated in the app. Not the end of the world, but it’s easy to miss a spot or forget about the setter function.

Search Flow in MERN Stack

First of all, I tried searching a lot but I am not able to find any resource which satisfies my need. I know there might be some answers already, if you know one please help with the link.
I know how to show search suggestions but I don't know how to show full search results when someone clicks on a search suggestion. Like how to do that in MERN stack with an example if possible.
I need a solution that best fits my scenario:
I have three models,
tags - holds tags
categories - holds categories
items - holds items data - has categories and tags both
currently, I am not storing references to categories and tags table instead
storing a copy directly inside items
Now, I basically want to search the items having the specific categories and tags when someone searches for a keyword.
What I am doing currently is, I search for tags matching the keyword, then categories, then taking out their _id(s) and finding that in items collection
const tags = await Tags.find(
{ tag: { $regex: category.toString(), $options: "i" } },
{ projection: { createdBy: 0 } });
const categories = await Categories.find(
{ category: { $regex: category.toString(), $options: "i" } },
{ projection: { createdBy: 0 } });
const tagsIdArray = tags.map((item) => new ObjectId(item._id));
const catIdArray = categories.map((item) => new
Object(item._id));
$match: {
$and: [
{
$or: [
{ "tags._id": { $in: [...tagsIdArray] } },
{ "category._id": { $in: [...catIdArray] } },
],},],},
And I know that this is not the best way, and it takes a lot of time to search for a given keyword.
Please suggest me schema structure and way to implement search with suggestions.

Many to Many relationships in Redux store

I created many to many relationship in Laravel and I want to create store with it. For example I have this code:
const INITIAL_PEOPLE = {
all: [
{name: "John", city: "NY"}
]
}
Any people has a tags (many to many relationships). The best way for me would be if my store look like this (unless you have another idea how can I implement and display elements with this relationship):
const INITIAL_PEOPLE = {
all: [
{name: "John", city: "NY", tags: [id: 1, name: "new_tag"]}
]
}
This is example like store looks but only data was the server and add by reducers. So how can I create this structure of store (or solve this problem with another way) with reducer?
You can normalize the data objects in api-response, before storing them into redux.
You can save the data as follows:
Assumption: There is a unique ID for each person.
ReduxStoreState:
{
"people":{
"id_1":{
"id":"id_1",
"name":"John",
"city":"NY"
},
"id_2":{
"id":"id_2",
"name":"Matt",
"city":"NJ"
}
},
"tagsByPeople":{
"id_1":[
{
"id":1,
"name":"new_tag"
}
]
}
}
Now you can write selectors to fetch Tags by providing person-ID

Best way to handle big dynamic form with stages in React?

I want to make an app where the admins can create "global" forms that other users can fill in. So I need these global forms to be dynamically rendered, and they are kind of big (30+ fields) and are divided in stages (e.g. stage 1 is for personal info, stage 2 is for job skills, etc).
I thought of receiving these "global" forms via JSON, something like this:
{
"filledBy":"User",
"stages":[
{
"id":1,
"name":"Personal information",
"fields":[
{
"id":1,
"type":"email",
"name":"email",
"label":"E-mail",
"placeholder":"name#company.com",
"value":"",
"rules":{
"required":true
}
},
{
"id":2,
"type":"text",
"name":"name",
"label":"Name",
"placeholder":"John Smith",
"value":"",
"pattern":"[A-Za-z]",
"rules":{
"required":true,
"minLength":2,
"maxLength":15
}
}
]
},
{
"id":2,
"name":"Job profile",
"fields":[
{
"id":1,
"type":"multi",
"name":"workExperience",
"subfields":[
{
"id":1,
"type":"text",
"name":"position",
"label":"Position",
"placeholder":"CEO",
"value":"",
"rules":{
"required":true,
"minLength":3,
"maxLength":30
}
},
{
"id":2,
"type":"date",
"name":"startDate",
"label":"Starting date",
"placeholder":"November/2015",
"value":"",
"rules":{
"required":true,
"minValue":"01/01/1970",
"maxValue":"today",
"showAsColumn":true
}
},
{
"id":3,
"type":"date",
"name":"endDate",
"label":"Ending date",
"placeholder":"March/2016",
"value":"",
"rules":{
"required":true,
"minValue":"endDate",
"maxValue":"today",
"showAsColumn":true
}
}
]
}
]
}
]
}
So I created a component called MasterForm that first gets the empty form in componentDidMount(), like a blueprint. Then, once it is fetched, it tries to get the data entered by the user and put it in the form as the value property. After that, it passes the form down to the Stage component which renders every field as an Input component. That way, MasterForm controls the current stage, and allows the user to navigate among stages, and also fetches the data and fills the form. With all the checks and stuff, my MasterForm component got very big (around 700 lines), and every time I update the value of a field in the form, I update the whole form object in the state, so I think that might be slow. Also, to fill in the form with the user's data, I have to copy every nested object and array inside the form object, to avoid mutating the state, and that's also very messy (a lot of const updatedFields = { ...this.state.form.stage.fields } and stuff).
Are there better ways to do this (preferably without Redux)? How could I decouple this huge MasterForm component? Is there a better way to update the form values (other than updating the whole form every time)? or maybe React is smart and doesn't update the whole state, but just the bit that changed... I'm not sure, I'm new to React.
Look into formik https://github.com/jaredpalmer/formik and Yup https://github.com/jquense/yup
Here they are coupled together https://jaredpalmer.com/formik/docs/guides/validation#validationschema

Modelling data based on a time range using redux/normalizr

I've have the following data coming from a node backend:
Endpoint:
/user/:id?startDate=2011-10-05T14:48:00.000Z&endDate=2011-10-05T14:48:00.000Z
[
{
"name": "Tom",
"createdAt": "2011-10-05T14:48:00.000Z",
"updatedAt": "2011-10-05T14:48:00.000Z",
...
"metadata": {
"activeHours": 134.45,
"afkHours": 134.45
}
},
{
...
}
]
In this data, the only thing modified between date changes is activeHours and afkHours.
These users and the date that the endpoint was called with must be synced across all pages.
The simple approach would be to put this in a users reducer, something like:
{
users: [...],
startDate: "",
endDate: ""
}
However I'm currently using normalizr with these users and with that I have a single action named ADD_ENTITIES. Having an entities reducer seems very beneficial as we do have other entities that can be nicely normalized with these users, however I don't want to pollute the entities state with almost "tacked on" startDate and endDate to sync across all pages.
On to my question:
Is there a better way to model this problem using normalizr, Where your key is not only ID but also a date range?
Or should I look at breaking this out into a separate reducer as above?
Not sure if I completely understood the problem here.
Is the startDate and endDate fields, different for each user ? If yes, then you may need to add those fields in the normalized entity object for those users.
If those are common fields for all the users, you can create a separate entity called userDateRange containing those two keys. It doesn't need to be normalized as they are primitive fields.
{
"entities": {
"user": {
"byId": {
"user1": {},
"user2": {}
},
"allIds": [
"user1",
"user2"
]
}
},
"ui": {
"userDateRange": {
"start": "2011-10-05T14:48:00.000Z",
"end": "2011-10-05T14:48:00.000Z"
}
}
}

Resources