How to create a dynamic table in React JS depending on keys?

I am trying to create a dynamic table component in React JS. The component currently only has a static header including the most common result keys. Some results also hold more information i.e. phone_number, degree. How do I dynamically extend the table with an additional column depending on presents of key/value? Should I work with state and make visible when present, or should I pre-process the table (but how do I ensure the correct order)? Are there better ways to do so?

import React, { Component } from 'react';

class ResultTable extends Component {

  addTableRow = (result) => {
    return (
      <tr key={result._id}>
        <td>{result.lastname}</td>
        <td>{result.firstname}</td>
        <td>{result.city}</td>
        <td>{result.zip}</td>
        <td>{result.street} {result.street_number}</td>
      </tr>)
  }

  createTable = (results) => {
    return (
      <table className="striped highlight ">
        <thead>
          <tr>
            <th>Lastname</th>
            <th>Firstname</th>
            <th>City</th>
            <th>ZIP</th>
            <th>Street</th>
          </tr>
        </thead>
        <tbody>
          {results.map((result, index) => {
            return this.addTableRow(result)
          })}
        </tbody>
      </table>
    )
  }

  render() {
    return (
      <div>
        {this.props.results.length ? (
          <div className="card">
            {this.createTable(this.props.results)}
          </div>
        ) : null}
      </div>
    )
  }
}

export default ResultTable

results look like that:

[
    { "_id": 1, "area_code": "555", "city": "Berlin", "firstname": "John", "lastname": "Doe", "phonenumber": "12345678", "zip": "12345"},
    { "_id": 2, "area_code": "555", "city": "Frankfurt", "firstname": "Arnold", "lastname": "Schwarzenegger", "phonenumber": "12121212", "street": "Main Street", "street_number": "99", "zip": "545454"},
    { "_id": 3, "area_code": "123", "firstname": "Jane", "lastname": "Doe", "phonenumber": "777777", "fav_color": "blue"},
    { "_id": 4, "area_code": "456", "firstname": "Scooby", "lastname": "Doo",  "phonenumber": "444444"}, "note": "Lazy but cool"
]

Answer

Alright alright, i took me around two hours to do it and to get back into react, it’s been a while i haven’t touched it. So if y’all see something to refactor, with pleasure you can edit !

The code first :

import React from "react";
import "./styles.css";

export default class App extends React.Component {
  state = {
    columns: [],
    columnsToHide: ["_id"],
    results: [
      {
        _id: 1,
        firstname: "Robert",
        lastname: "Redfort",
        city: "New York",
        zip: 1233,
        street: "Mahn Street",
        street_number: "24A",
        favoriteKebab: "cow"
      },
      {
        _id: 2,
        firstname: "Patty",
        lastname: "Koulou",
        city: "Los Angeles",
        zip: 5654,
        street: "Av 5th Central",
        street_number: 12
      },
      {
        _id: 3,
        firstname: "Matt",
        lastname: "Michiolo",
        city: "Chicago",
        zip: 43452,
        street: "Saint Usk St",
        street_number: 65,
        phoneNumber: "0321454545"
      },
      {
        _id: 4,
        firstname: "Sonia",
        lastname: "Remontada",
        city: "Buenos Aires",
        zip: "43N95D",
        street: "Viva la Revolution Paso",
        street_number: 5446,
        country: "Argentina"
      }
    ]
  };
  componentDidMount() {
    this.mappDynamicColumns();
  }

  mappDynamicColumns = () => {
    let columns = [];
    this.state.results.forEach((result) => {
      Object.keys(result).forEach((col) => {
        if (!columns.includes(col)) {
          columns.push(col);
        }
      });
      this.setState({ columns });
    });
  };

  addTableRow = (result) => {
    let row = [];
    this.state.columns.forEach((col) => {
      if (!this.state.columnsToHide.includes(col)) {
        row.push(
          Object.keys(result).map((item) => {
            if (result[item] && item === col) {
              return result[item];
            } else if (item === col) {
              return "No Value";
            }
          })
        );
        row = this.filterDeepUndefinedValues(row);
      }
    });

    return row.map((item, index) => {
      // console.log(item, "item ?");
      return (
        <td
          key={`${item}--${index}`}
          className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"
        >
          {item}
        </td>
      );
    });
  };

  mapTableColumns = () => {
    return this.state.columns.map((col) => {
      if (!this.state.columnsToHide.includes(col)) {
        const overridedColumnName = this.overrideColumnName(col);
        return (
          <th
            key={col}
            scope="col"
            className="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
          >
            {overridedColumnName}
          </th>
        );
      }
    });
  };

  filterDeepUndefinedValues = (arr) => {
    return arr
      .map((val) =>
        val.map((deepVal) => deepVal).filter((deeperVal) => deeperVal)
      )
      .map((val) => {
        if (val.length < 1) {
          val = ["-"];
          return val;
        }
        return val;
      });
  };

  // if you want to change the text of the col you could do here in the .map() with another function that handle the display text

  overrideColumnName = (colName) => {
    switch (colName) {
      case "phoneNumber":
        return "Phone number";
      case "lastname":
        return "Custom Last Name";
      default:
        return colName;
    }
  };

  createTable = (results) => {
    return (
      <table class="min-w-full divide-y divide-gray-200">
        <thead>
          <tr>{this.mapTableColumns()}</tr>
        </thead>
        <tbody>
          {results.map((result, index) => {
            return <tr key={result._id}>{this.addTableRow(result)}</tr>;
          })}
        </tbody>
      </table>
    );
  };

  render() {
    return (
      <div class="flex flex-col">
        <div class="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
          <div class="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
            <div class="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
              {this.state.results.length ? (
                <div className="card">
                  {this.createTable(this.state.results)}
                </div>
              ) : null}
            </div>
          </div>
        </div>
      </div>
    );
  }
}

Why undefined ? Well, like i said, im kind of rotten on react (and maybe on codding 😉 ) But i didn’t find any other way to keep the orders of each value under her belonging column without it being at her current index in the array.

To come back to our code, we end up with a variable row populated with each values belonging to her current column name, so at the right index.

the payload of row before being sending back on the html is so :

As you see, it isn’t clean. But i did not find any solution for that, i believe it’s possible but it requires more time to think.

So since it’s really deeply nested, i had to come up with some filters, to allow the user to see when the property doens’t exist, or we could just left it blank.

After each push, i need to clean my deep nested values in arrays, so i’ve come up with a function filterDeepUndefinedValues row = this.filterDeepUndefinedValues(row)

And the function itself

filterDeepUndefinedValues = (arr) => {
  return arr
    .map((val) => val.map((deepVal) => deepVal).filter((deeperVal) => deeperVal))
    .map((val) => {
      if (val.length < 1) {
        val = ["-"];
        return val;
      }
      return val;
    });
};

To make short, i have arrays nested in array which contain undefined values, so i’ll return a new array filtered out without undefined value, so it return an empty array.

How the array is received in the function initially enter image description here

In the second part, i just replace empty array content by a hyphen in array

First treatment with .map and filter() enter image description here

Replace empty array with a hyphen enter image description here

The return of addTableRow

return row.map((item, index) => {
  return (
    <td
      key={`${item}--${index}`}
      className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"
    >
      {item}
    </td>
  );
});

Here we simply map all the values from rows and render a td for each array of row

Im running out of time to end the details, but all it’s here and i will try to come back later to clean a bit this answer and it’s grammar.

Codesandbox link: https://codesandbox.io/s/bold-mendel-vmfsk?file=/src/App.js