3

I have just started learning React

I am working on solution where i want to create tree like table using react.

So basically functionality is like there is simple table with each row having conditional expand icon, i achieved basic functionality of rendering another row component when i click on row.

Basic solution that i have achieved is whenever user clicks on any i call function expand row add static children under that clicked row array and display it using passing children object to subrow component.

Now i want whenever user clicks on expanded row children, it should expand to next level having displaying data related to second expand.

So basically it will look like

- Row 1
  - Child 1
    - Child 1.1
    - Child 1.2
  + Child 2
+ Row 2

I have created basic prototype using static json data from jsonplaceholder apis

Here is code

App.js

import React, { useState, Fragment } from "react";
import TableRowData from "./TableRowData";
import "./styles.css";

export default function App() {
  const [splits, setSplit] = useState(["campid", "appid", "os"]);
  return (
    <Fragment>
      <table>
        <thead>
          <tr>
            <th>Id</th>
            <th>Name</th>
          </tr>
        </thead>
        <tbody>
          <TableRowData avaliableSplits={splits} />
        </tbody>
      </table>
    </Fragment>
  );
}

TableRowData.js

import React, { useState, useEffect, useRef, Fragment } from "react";
import axios from "axios";
import SubRow from "./SubRow";

class TableRowData extends React.Component {
  state = { data: [] };
  constructor(props) {
    super(props);
  }

  componentDidMount() {
    axios.get("https://jsonplaceholder.typicode.com/users").then((res) => {
      this.setState({ data: res.data });
    });
  }
  render() {
    const updateState = (id, itemAttributes) => {
      var index = this.state.data.findIndex((x) => x.id === id);
      if (index !== -1) {
        this.setState({
          data: [
            ...this.state.data.slice(0, index),
            Object.assign({}, this.state.data[index], itemAttributes),
            ...this.state.data.slice(index + 1)
          ]
        });
      }
    };

    const expandRow = (user) => {
      user.children = [
        { id: "6656", name: "sfsdfds1" },
        { id: "66563", name: "sfsdfds2" }
      ];

      //    this.setState({data:[...this.state.data],})
      updateState(user.id, { isExpanded: true });
    };

    const collapseRow = (user) => {
      user.children = undefined;
      updateState(user.id, { isExpanded: false });
    };

    if (this.state.data) {
      const appData = this.state.data.map((user) => {
        return (
          <Fragment key={user.id}>
            <tr key={user.id}>
              <td>
                {user.isExpanded === true ? (
                  <button type="button" onClick={() => collapseRow(user)}>
                    -
                  </button>
                ) : (
                  <button type="button" onClick={() => expandRow(user)}>
                    +
                  </button>
                )}
                {user.id}
              </td>
              <td>{user.name}</td>
            </tr>
            {!!user.children && <SubRow rowData={user.children} />}
          </Fragment>
        );
      });
      return <Fragment>{appData}</Fragment>;
    } else {
      return null;
    }
  }
}

export default TableRowData;

SubRow.js

import React, { useState, useEffect, useRef, Fragment } from 'react';
import axios from 'axios';

const SubRow = (props) => {
    const appData = props.rowData.map((user) => {
            user.isExpanded = false;
            return (
                    <Fragment key={user.id}>
                        <tr key={user.id}>
                            <td><button type='button' onClick={()=>handleClick(user,props.reportData)}>+</button>{user.id}</td>
                            <td>{user.name}</td>
                        </tr>
                         {!!user.children && <SubRow rowData={user.children} />}
                    </Fragment>
                  )
                
             });

        return (
            <Fragment>{appData}</Fragment>
        )
}
    
 export default SubRow

Here is codesandbox implementation Nested table

I do not want to use any external packages for same. Please help

Adding conditional expand collapse scenario

I want to make expand conditional based on array i am maintaining

Lets say i have an array splits [a,b,c], if value set to this array first level row will have data related to A now whenever use clicks on B, i will make an AJAX request with row-data of A and display rows of B with expand icon as this will be 3 level tree table, similarly whenever user clicks on C i will send data of b and retrieve data of C. now D will not have any further expand as array size is 3, whenever user adds 4th element in array i have to saw expand icon on C.

Attemp 1:

import React, { useState, useEffect, useRef, Fragment } from "react";
import _ from 'lodash';
import axios from "axios";
import {connect} from 'react-redux';
import {getChildren} from '@src/redux/actions/reports';

class TableRowData extends React.Component {
  state = {
    showIcon: false,
    selection: [],
    data: []
  };
  constructor(props) {
    super(props);
  }

  componentDidMount() {
    axios.get("https://jsonplaceholder.typicode.com/users").then((res) => {
          const rowData = res.data.map((row) => {
              row.isExpanded = false;
              return row;
          });
          this.setState({ data: rowData });
    });

  }

  render() {
    const updateState = (id, itemAttributes) => {
      var index = this.state.data.findIndex((x) => x.id === id);
      if (index !== -1) {
        this.setState({
          data: [
            ...this.state.data.slice(0, index),
            Object.assign({}, this.state.data[index], itemAttributes),
            ...this.state.data.slice(index + 1)
          ]
        });
      }
    };

    const expandRow = (row) => {
      const index = _(this.state.data)
        .thru(function(coll) {
            return _.union(coll, _.map(coll, 'children') || []);
        })
        .flattenDeep()
        .findIndex({ id: row.id });


       if (index !== -1) {
        let prevState = [...this.state.data];
        let el = _(prevState)
          .thru(function(coll) {
              return _.union(coll, _.map(coll, 'children') || []);
          })
          .flattenDeep()
          .find({ id: row.id });
        el.children = [
          { id: '_' + Math.random().toString(36).substr(2, 5), name: row.id+"_ID1", isExpanded:false,parentId:row.id },
          { id: '_' + Math.random().toString(36).substr(2, 5), name: row.id+"_ID2ß",isExpanded:false,parentId:row.id },
        ];
        el.isExpanded=true;
        this.setState({data:[...this.state.data],prevState},()=>{})
      }
    };

    const collapseRow = (user) => {
      delete user.children
    //  updateState(user.id, { children: {id:1,name:'JANAK'} });
      updateState(user.id, { isExpanded: false });
    };

    const ExpandableTableRow = ({rows}) => {
      //console.log(rows);
      if (rows) {
          return rows.map((row) => {
          let children = null;

            return (
                <Fragment key={row.id}>
                  <tr key={row.id}>
                  <td>
                      <ExpandCollapsToggle row={row} /> {row.id}
                  </td>
                    <td>{row.name}</td>
                </tr>
                  <ExpandableTableRow rows={row.children} />
              </Fragment>
            )
           }
        );

        } else {
          return null;
        }
    };

    const ExpandCollapsToggle = ({row,actions}) => {
        if(row.isExpanded === true) {
              return (<button type="button" onClick={() => collapseRow(row)}>-</button>)
          } else {
              return (<button type="button" onClick={() => expandRow(row)}>+</button>)
          }
    }

    if (this.state.data) {
        return (
          <Fragment>
              <ExpandableTableRow rows={this.state.data} />
          </Fragment>
        );
    } else {
      return null;
    }
  }
}
const mapStateToProps = (state) => {
  return {"data":state.reportReducer.data};
//  return state;
}
export default connect(mapStateToProps,{getChildren})(TableRowData);
4
  • 1
    what is your question? what doesn't work now? Commented Feb 17, 2021 at 9:45
  • Nested table works but i would like to implement it for multiple levels using one component only, Sorry for bad explanation You can check codesandbox click on any parent node once child rows are there i want that child row clickable and also should render their child under clicked row Commented Feb 17, 2021 at 9:57
  • Can you please explain a little bit more, What I understand from your question is that you want to use expandrow() function from TableRowData.js in your SubRow.js? Commented Feb 19, 2021 at 10:08
  • @PadminiS Hi it's not like that exactly I want to create nested table using single component based on some conditions whenevery user clicks on expand button, i will make API call and render rows below expanded button row, it should be work for n-level there are no restrictions on expand Commented Feb 19, 2021 at 10:13

1 Answer 1

3
+100

Solution 1

import React, { useRef, useState, useEffect } from "react";
import axios from 'axios';
import "./style.css";

function ExpandableRow({
  loadData = new Promise(resolve => resolve([])),
  onExpand,
  columns,
  id,
  row
  }) {
  /* added condition to test if not first render
   * if yes then added data through reference which
   * will update the state but because state is
   * updated directly render would not occur
   * to handle this issue `onExpand` function
   * is passed from parent to child which will update
   * state with new data which will intern cause rendering
   */
  const [rowState, setRowState] = useState(1);
  const didMount = useRef(false);

  useEffect(() => {
    if (didMount.current) {
      if (row.hasOwnProperty("children")) {
        delete row["children"];
        onExpand("remove", id, row);
      } else {
        setRowState(2);
        loadData(id, row).then(data => {
          row['children'] = data;
          onExpand("add", id, row);
          setRowState(3);
        });
      }
    } else didMount.current = true;
  }, [rowState]);

  return (
    <tr>
      <td>
        <button
          type="button"
          onClick={() => {
            setRowState(rowState == 3 ? 1 : 3);
          }}
        >
          {rowState == 2 ? "o" : rowState == 3 ? '-' : "+"}
        </button>
      </td>

      {columns.map((column, idx) => (
        <td key={idx}>{row[column]}</td>
      ))}
    </tr>
  );
}

// function for recursively creating table rows
function ExpandableRowTable({ loadData, onExpand, columns, rows }) {
  return rows.map((row, idx) => {
    const actions = {
      loadData,
      onExpand
    };

    return (
      <React.Fragment>
        <ExpandableRow
          id={idx}
          columns={columns}
          row={row}
          {...actions}
        />
        {row.children && (
          <ExpandableRowTable
            columns={columns}
            rows={row.children}
            {...actions}
          />
        )}
      </React.Fragment>
    );
  });
}

function apiData() {
  return axios.get("https://jsonplaceholder.typicode.com/users")
  .then((res) => {
    return res.data;
  });
}

let count = 0;
const columns = ["id", "name"];
export default function App() {
  const [rows, setRows] = useState([]);

  useEffect(() => {
    apiData().then((data) => {
      count = data.length;
      setRows(data)
    })
    .catch(err => {
      console.log(err);
    })
  }, []);

  return (
    <div>
      <table>
        <thead>
          <tr>
            <th />
            {columns.map((c, idx) => (
              <th key={idx}>{c}</th>
            ))}
          </tr>
        </thead>
        <tbody>
          <ExpandableRowTable
            loadData={(id, row) => {
              return new Promise((resolve) => {
                setTimeout(() => {
                  resolve([
                    {
                      id: count + 1,
                      name: `- ${row["name"]}`
                    },
                    {
                      id: count + 2,
                      name: `- ${row["name"]}`
                    }
                  ]);
                  count += 2
                }, 1000);
              })
            }}
            onExpand={(action, id, row) => {
              if (action == "remove") {
                count -= 2
              }
              setRows([...rows]);
            }}
            columns={columns}
            rows={rows}
          />
        </tbody>
      </table>
    </div>
  );
}

Solution 1 working example

Solution 2

import React, {
  createContext,
  useContext,
  useRef,
  useState,
  useReducer,
  useEffect
} from "react";
import axios from "axios";
import "./style.css";

const data = {
  rows: [
    {
      id: 1,
      name: "leanne graham"
    },
    {
      id: 2,
      name: "ervin howell"
    },
    {
      id: 3,
      name: "clementine bauch"
    }
  ],
  options: {
    "": null,
    cities: [
      {
        id: 1,
        name: "delhi"
      },
      {
        id: 2,
        name: "pune"
      },
      {
        id: 3,
        name: "hyderabad"
      }
    ],
    network: [
      {
        id: 1,
        name: "unknown"
      },
      {
        id: 2,
        name: "known"
      },
      {
        id: 3,
        name: "KNOWN"
      }
    ]
  }
};

const initialState = {
  showIcon: false,
  selection: [],
  rows: []
};

const store = createContext(initialState);
const { Provider, Consumer } = store;

const StateProvider = ({ children }) => {
  const [state, dispatch] = useReducer((state, action) => {
    // console.log(state.rows);
    switch (action.type) {
      case "ADD_SELECTION":
        if (state.selection.indexOf(action.payload) == -1) {
          const newState = {...state};
          newState['selection'].push(action.payload);
          return newState;
        }
        return state;
      case "SET_ROWS":
        return Object.assign({}, state, {
          rows: action.payload
        });
      case "TOGGLE_ICON":
        return Object.assign({}, state, {
          showIcon: !state.showIcon
        });
      default:
        return state;
    }
  }, initialState);

  return (
    <Provider
      value={{
        state,
        dispatch,
        toggleIcon: () => {
          dispatch({
            type: "TOGGLE_ICON"
          });
        },
        addSelection: value => {
          dispatch({
            type: "ADD_SELECTION",
            payload: value
          });
        },
        setRows: rows => {
          dispatch({
            type: "SET_ROWS",
            payload: rows
          });
        }
      }}
    >
      {children}
    </Provider>
  );
};
/*
1 - Loading (o)
2 - Plus    (+)
3 - Minus   (-)
*/
function ExpandableRow({
  showIcon,
  loadData = new Promise(resolve => resolve([])),
  onExpand,
  columns,
  id,
  row
}) {
  /* added condition to test if not first render
   * if yes then added data through reference which
   * will update the state but because state is
   * updated directly render would not occur
   * to handle this issue `onExpand` function
   * is passed from parent to child which will update
   * state with new data which will intern cause rendering
   */
  const [rowState, setRowState] = useState(2);
  const didMount = useRef(false);

  useEffect(() => {
    if (didMount.current) {
      if (row.hasOwnProperty("children")) {
        if (rowState == 2) {
          delete row["children"];
          onExpand("remove", id, row);
        }
      } else {
        setRowState(1);
        didMount.current = false;
        loadData(id, row).then(_data => {
          row["children"] = _data;
          onExpand("add", id, row);
          setRowState(3);
        });
      }
    } else didMount.current = true;
  }, [rowState]);

  return (
    <tr>
      <td>
        {showIcon && (
          <button
            type="button"
            onClick={() => {
              setRowState(rowState == 3 ? 2 : 3);
            }}
          >
            {rowState == 1 ? "o" : rowState == 3 ? "-" : "+"}
          </button>
        )}
      </td>

      {columns.map((column, idx) => (
        <td key={idx}>{row[column]}</td>
      ))}
    </tr>
  );
}

// function for recursively creating table rows
function ExpandableRowTable({ showIcon, loadData, onExpand, columns, rows }) {
  return rows.map((row, idx) => {
    const actions = {
      loadData,
      onExpand
    };

    return (
      <React.Fragment key={idx}>
        <ExpandableRow
          id={idx}
          showIcon={showIcon}
          columns={columns}
          row={row}
          {...actions}
        />
        {row.children && (
          <ExpandableRowTable
            showIcon={showIcon}
            columns={columns}
            rows={row.children}
            {...actions}
          />
        )}
      </React.Fragment>
    );
  });
}

function apiData(requestData) {
  console.log(requestData);
  return axios.get("https://jsonplaceholder.typicode.com/users").then(res => {
    return res.data;
  });
}

const columns = ["id", "name"];
function Select(props) {
  const { state, toggleIcon, addSelection } = useContext(store);
  const { selection } = state;
  return (
    <div>
      <div>{selection.join(", ")}</div>
      <select
        value={""}
        onChange={evt => {
          selection[selection.length - 1] != evt.target.value && toggleIcon();
          addSelection(evt.target.value);
        }}
      >
        {Object.keys(data.options).map((c, idx) => (
          <option key={idx}>{c}</option>
        ))}
      </select>
    </div>
  );
}

function Table(props) {
  const { state, toggleIcon, setRows } = useContext(store);
  const { rows, selection } = state;

  useEffect(() => {
    // apiData().then((data) => {
    setRows(data.rows);
    // })
    // .catch(err => {
    //   console.log(err);
    // })
  }, []);

  return (
    <table>
      <thead>
        <tr>
          <th />
          {columns.map((c, idx) => (
            <th key={idx}>{c}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        <ExpandableRowTable
          showIcon={state.showIcon}
          loadData={(id, row) => {
            return new Promise(resolve => {
              const lastSelection = selection[selection.length - 1];
              setTimeout(() => {
                resolve(data.options[lastSelection]);
              }, 1000);
            });
          }}
          onExpand={(action, id, row) => {
            setRows(rows);
            toggleIcon();
          }}
          columns={columns}
          rows={rows}
        />
      </tbody>
    </table>
  );
}

export default function App() {
  return (
    <div>
      <StateProvider>
        <Select />
        <Table />
      </StateProvider>
    </div>
  );
}

Solution 2 working example

Solution 3

import React, { useState, useEffect, useRef, Fragment } from "react";
import axios from "axios";

class TableRowData extends React.Component {
  state = {
    showIcon: false,
    selection: [],
    data: []
  };
  constructor(props) {
    super(props);
  }

  componentDidMount() {
    axios.get("https://jsonplaceholder.typicode.com/users").then((res) => {
      const rowData = res.data.map((row) => {
        row.isExpanded = false;
        return row;
      });
      this.setState({ data: rowData });
    });
  }

  render() {
    const updateState = () => {
      this.setState(
        {
          data: [...this.state.data]
        },
        () => {}
      )
    }

    const ExpandableTableRow = ({ rows }) => {
      const expandRow = (row) => {
          row.children = [
            {
              id: "_" + Math.random().toString(36).substr(2, 5),
              name: row.id + "_ID1",
              isExpanded: false,
              parentId: row.id
            },
            {
              id: "_" + Math.random().toString(36).substr(2, 5),
              name: row.id + "_ID2ß",
              isExpanded: false,
              parentId: row.id
            }
          ];
          row.isExpanded = true;
          updateState();
      };

      const collapseRow = (row) => {
        delete row.children;
        row.isExpanded = false;
        updateState();
      };

      const ExpandCollapsToggle = ({ row, expandRow, collapseRow }) => {
        return (
          <button type="button" onClick={() => row.isExpanded ? collapseRow(row) : expandRow(row)}>
            {row.isExpanded ? '-' : '+'}
          </button>
        );
      };

      if (rows) {
        return rows.map((row) => {
          let children = null;
          return (
            <Fragment key={row.id}>
              <tr key={row.id}>
                <td>
                  <ExpandCollapsToggle
                    row={row}
                    expandRow={expandRow}
                    collapseRow={collapseRow}
                  />{" "}
                  {row.id}
                </td>
                <td>{row.name}</td>
              </tr>
              <ExpandableTableRow rows={row.children} />
            </Fragment>
          );
        });
        return null;
    };

    if (this.state.data) {
      return (
        <Fragment>
          <ExpandableTableRow rows={this.state.data} />
        </Fragment>
      );
    }
    return null;
  }
}
const mapStateToProps = (state) => {
  return { data: state.reportReducer.data };
};
export default TableRowData;

Solution 3 working example

Sign up to request clarification or add additional context in comments.

35 Comments

Can you please add some explanation ? is this going to work with axios call as well ?
@JanakPrajapati yes it will work with axios i just removed axios because i don't want to wait for data each and every time while developing the code.
@JanakPrajapati can you share angular project in which you have create the same functionality
Hi @chandan: i would like to continue with simple class based solution Just need little help i am not able to pass children recursively in my component, inspired by your solution i tried to achieve basic functionality using react Here is class based component solution it works till 3 level only, if you can help me to figure out it'll be great, may be little tweak is required but i am out of my mind :( codesandbox.io/s/exciting-dust-0479m
@JanakPrajapati sorry i am little busy will check when free.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.