reactjsstructureantddata-transformcode-structure

Improve reusability of my data transformation functions


I have currently done with my code transformation on my api data by its roleId. However, I need to display another view which will group the data according to which projectId the users are in.

I can simply copy and paste and create another one for projectIds data transformation, however, I feel that my approach may be messy and not easily reusable. So I would like to ask if there is a better way to do this?

Because i cannot simply swap roleIds in the function to projectIds by putting rolesId or projectIds in a variable to be reused in the function.

Can anyone help me please ?

Code for the api data transformation to dislay in ant design tree data table:

let apiData = [
  {
    email: "alyssayo@xxx.com",
    permissionIds: null,
    roleIds: ["raa", "baa", "caa"],
    projectIds: ["1aa", "3aa"]
  },
  {
    email: "chiuewww@xxx.com",
    permissionIds: null,
    roleIds: ["baa", "caa"],
    projectIds: ["1aa", "2aa", "3aa"]
  },
  {
    email: "lalaqq@xxx.com",
    permissionIds: null,
    roleIds: ["caa"],
    projectIds: ["1aa"]
  },
  {
    email: "sqssq@xxx.com",
    permissionIds: null,
    roleIds: [],
    projectIds: []
  }
];

        //Isolate and transform data by roleId
        const transData = apiData.reduce((arr, item) => {
          let formatted = item.roleIds.map((id) => {
            return {
              roleIds: id,
              children: [{ ...item, roleIds: id }]
            };
          });

          return [...arr, ...formatted];
        }, []);

        //Group transformed data by roleIds
        const findMatch = (arr, roleIds) =>
          arr.find((item) => item.roleIds === roleIds);

        const groupArray = (originalArr) => {
          return Array.isArray(originalArr)
            ? originalArr.reduce((previousObj, obj) => {
              if (findMatch(previousObj, obj.roleIds)) {
                findMatch(previousObj, obj.roleIds).children.push(...obj.children);
              } else {
                previousObj.push(obj);
              }
              return previousObj;
            }, [])
            : "Need an array";
        };
        //Call the group roleId function on transformed data by roleId
        const userRoledata = groupArray(transData);

        //Add key to parent and children
        let key = 1;
        userRoledata.forEach((item) => {
          item.key = key++;
          item.children.forEach((child) => {
            child.key = key++;
          });
        });

        setData(userRoledata); //this will be dataSource for table rendering in ant design

What will the data transformed display when used as dataSource in ant design:

If grouped by roleIds:

[
  {
    "roleIds": "raa",
    "children": [
      {
        "email": "alyssayo@xxx.com",
        "permissionIds": null,
        "roleIds": "raa",
        "projectIds": [
          "1aa",
          "3aa"
        ],
        "key": 2
      }
    ],
    "key": 1
  },
  {
    "roleIds": "baa",
    "children": [
      {
        "email": "alyssayo@xxx.com",
        "permissionIds": null,
        "roleIds": "baa",
        "projectIds": [
          "1aa",
          "3aa"
        ],
        "key": 4
      },
      {
        "email": "chiuewww@xxx.com",
        "permissionIds": null,
        "roleIds": "baa",
        "projectIds": [
          "1aa",
          "2aa",
          "3aa"
        ],
        "key": 5
      }
    ],
    "key": 3
  },
  {
    "roleIds": "caa",
    "children": [
      {
        "email": "alyssayo@xxx.com",
        "permissionIds": null,
        "roleIds": "caa",
        "projectIds": [
          "1aa",
          "3aa"
        ],
        "key": 7
      },
      {
        "email": "chiuewww@xxx.com",
        "permissionIds": null,
        "roleIds": "caa",
        "projectIds": [
          "1aa",
          "2aa",
          "3aa"
        ],
        "key": 8
      },
      {
        "email": "lalaqq@xxx.com",
        "permissionIds": null,
        "roleIds": "caa",
        "projectIds": [
          "1aa"
        ],
        "key": 9
      }
    ],
    "key": 6
  }
]

If grouped by projectIds:

[
  {
    "projectIds": "1aa",
    "children": [
      {
        "email": "alyssayo@xxx.com",
        "permissionIds": null,
        "roleIds": [
          "raa",
          "baa",
          "caa"
        ],
        "projectIds": "1aa",
        "key": 2
      },
      {
        "email": "chiuewww@xxx.com",
        "permissionIds": null,
        "roleIds": [
          "baa",
          "caa"
        ],
        "projectIds": "1aa",
        "key": 3
      },
      {
        "email": "lalaqq@xxx.com",
        "permissionIds": null,
        "roleIds": [
          "caa"
        ],
        "projectIds": "1aa",
        "key": 4
      }
    ],
    "key": 1
  },
  {
    "projectIds": "3aa",
    "children": [
      {
        "email": "alyssayo@xxx.com",
        "permissionIds": null,
        "roleIds": [
          "raa",
          "baa",
          "caa"
        ],
        "projectIds": "3aa",
        "key": 6
      },
      {
        "email": "chiuewww@xxx.com",
        "permissionIds": null,
        "roleIds": [
          "baa",
          "caa"
        ],
        "projectIds": "3aa",
        "key": 7
      }
    ],
    "key": 5
  },
  {
    "projectIds": "2aa",
    "children": [
      {
        "email": "chiuewww@xxx.com",
        "permissionIds": null,
        "roleIds": [
          "baa",
          "caa"
        ],
        "projectIds": "2aa",
        "key": 9
      }
    ],
    "key": 8
  }
]

Solution

  • Define a transform function with 2 parameter. First the apiData, which is the data you want to transform and secondly the transformation_key which is a string of either roleIds or projectIds.

    Within this function you first to generate an object with the different roleIds/projectIds as keys and for each key an array of all the items included.

    To do so you make use of a reducer and loop over the items

    apiData.reduce((obj, item) => {
      if (!item[transformation_key]) return obj; // in case item[transformation_key] is null, you can skip the item an just return the item as is.
      ... // if not, we've to reduce over the array of roleIds/projectIds within the item as well.
    }, {}) // {} is the new object (`obj` refers to this within the reducer)
    

    Within each item we also have to loop over all the items in the roleIds/projectIds of that item, so we add a second/inner reducer.

    // item[transformation_key] is the array of roleIds/projectIds within your item.
    item[transformation_key].reduce((cur, id) => {
      // `cur` is actually the same object as the `obj` from the outer reducer.
      if (!cur[id]) cur[id] = [] // if the key/id doesn't excist yet on the object, we set it equal to an empty array.
      cur[id].push({
        ...item,
        [transformation_key]: id
      }) // we push the item to the array (using the spread operator and updating the value for the `transformation_key` within the item.
      return cur // you must return the object `cur`.
    }, obj) // we pass to `obj` from the outer reducer into the inner reducer.
    

    This will generate an object like

    const transformedObject = {
      [roleIds/projectIds] : [
        ... all the children
      ]
    }
    

    next we map the ids to the required output

    return Object.keys(transformedObject).map(key => {
      return {
        [transformation_key]: key,
        children: transformedObject[key]
      }
    })
    

    To summarize

    function transform(apiData, transformation_key) {
    
      if (!(transformation_key == 'roleIds' || transformation_key == 'projectIds')) throw new Error("Choose either 'roleIds' or 'projectIds' as a transformation_key")
    
      const transformedObject = apiData
        .reduce((obj, item) => {
          if (!item[transformation_key]) return obj;
          return item[transformation_key].reduce((cur, id) => {
            if (!cur[id]) cur[id] = []
            cur[id].push({
              ...item,
              [transformation_key]: id
            })
            return cur
          }, obj)
        }, {});
        
      return Object.keys(transformedObject).map(key => {
        return {
          [transformation_key]: key,
          children: transformedObject[key]
        }
      })
    }
    
    const transDataByRoleIds = transform(res.data, 'roleIds')
    const transDataByProjectIds = transform(res.data, 'projectIds')