首页 > 解决方案 > 如何使用 Recursion js 使用递归创建树表?

问题描述

我刚开始学习 React

我正在研究我想使用反应创建树状表的解决方案。

所以基本上功能就像有一个简单的表格,每行都有条件展开图标,当我点击行时,我实现了渲染另一个行组件的基本功能。

我实现的基本解决方案是,每当用户单击任何我调用的函数展开行时,都会在该单击的行数组下添加静态子项,并使用将子对象传递给子行组件来显示它。

现在我想每当用户点击展开的行子级时,它应该展开到下一个级别,显示与第二次展开相关的数据。

所以基本上它看起来像

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

我使用来自 jsonplaceholder apis 的静态 json 数据创建了基本原型

这是代码

应用程序.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;

子行.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

这是codesandbox实现嵌套表

我不想使用任何外部软件包。请帮忙

添加有条件的展开折叠场景

我想根据我正在维护的数组进行扩展

假设我有一个数组拆分 [a,b,c],如果设置为此数组第一级行的值现在将具有与 A 相关的数据,每当使用 B 时,我将使用 A 的行数据发出 AJAX 请求和用展开图标显示 B 的行,因为这将是 3 级树形表,类似地,每当用户单击 C 时,我将发送 b 的数据并检索 C 的数据。现在 D 将不会有任何进一步扩展,因为数组大小为 3,每当用户在数组中添加第 4 个元素我必须在 C 上看到展开图标。

尝试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);

标签: reactjsreact-native

解决方案


解决方案 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>
  );
}

解决方案 1 工作示例

解决方案 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>
  );
}

解决方案 2 工作示例

解决方案 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;

解决方案 3 工作示例


推荐阅读