It’s widely acknowledged that creating a table with React is a pain. No surprise, then, that there are many libraries to make creating tables easier for React apps. One of these packages is react-table
. It provides a modern, Hooks-based API to let us create tables in React with little hassle.
With the latest major release, React Table v7, creator Tanner Linsley aimed to refactor the entire library to a UI-, style-, and markup-agnostic table building tool that uses Hooks exclusively.
In this tutorial, we’ll tell you all you need to know about the latest version of react-table
(at the time of writing, the most recent release is React Table v7.6.3), outline the major changes and new features shipped with React Table v7, and see them in action with a basic example.
We’ll cover the following:
In March 2020, React Table creator Tanner Linsley released React Table v7, which he described as “the culmination of over a years [sic] worth of work to refactor the entire library to a hooks-only UI/Style/Markup agnostic table building utility.”
React Table v7 is comprised of a collection of React Hooks and plugins designed to help you compose logical features of complex data grids into a single, performant, extensible, and unopinionated API, which is returned by the primary useTable
hook.
As a headless utility, React Table v7 doesn’t render or supply data table UI elements out of the box. That means you’re responsible for rendering your own table markup using the state and callback of the hooks provided by React Table.
According to the release notes, React Table 7 introduced the following features to the library:
The changes and new features in the latest release are meant to make React Table more:
As mentioned above, the most recent minor release of react-table
is React Table v7.6.3, released on Jan. 11, 2021. Head to npm for a full version history of React Table.
Creating a basic table in a React app is easy with react-table. Run the following to install it:
npm i react-table
Then we can use it as follows:
import React from "react"; import { useTable } from "react-table"; const data = [ { firstName: "jane", lastName: "doe", age: 20 }, { firstName: "john", lastName: "smith", age: 21 } ]; const columns = [ { Header: "Name", columns: [ { Header: "First Name", accessor: "firstName" }, { Header: "Last Name", accessor: "lastName" } ] }, { Header: "Other Info", columns: [ { Header: "Age", accessor: "age" } ] } ]; const Table = ({ columns, data }) => { const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable({ columns, data }); return ( <table {...getTableProps()}> <thead> {headerGroups.map(headerGroup => ( <tr {...headerGroup.getHeaderGroupProps()}> {headerGroup.headers.map(column => ( <th {...column.getHeaderProps()}>{column.render("Header")}</th> ))} </tr> ))} </thead> <tbody {...getTableBodyProps()}> {rows.map((row, i) => { prepareRow(row); return ( <tr {...row.getRowProps()}> {row.cells.map(cell => { return <td {...cell.getCellProps()}>{cell.render("Cell")}</td>; })} </tr> ); })} </tbody> </table> ); }; export default function App() { return ( <div className="App"> <Table columns={columns} data={data} /> </div> ); }
In the code above, we imported the useTable
Hook from the react-table package. Then we created the data
to populate the table with data:
const data = [ { firstName: "jane", lastName: "doe", age: 20 }, { firstName: "john", lastName: "smith", age: 21 } ];
We just put properties in objects to add additional data for a table row.
We can create columns in a list with the following code:
const columns = [ { Header: "Name", columns: [ { Header: "First Name", accessor: "firstName" }, { Header: "Last Name", accessor: "lastName" } ] }, { Header: "Other Info", columns: [ { Header: "Age", accessor: "age" } ] } ];
The Header
property has the string for the names that’ll be displayed, and the accessor
property is the property name that’s in the array entry objects.
In the Table
component code, we have:
const Table = ({ columns, data }) => { const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable({ columns, data }); return ( <table {...getTableProps()}> <thead> {headerGroups.map(headerGroup => ( <tr {...headerGroup.getHeaderGroupProps()}> {headerGroup.headers.map(column => ( <th {...column.getHeaderProps()}>{column.render("Header")}</th> ))} </tr> ))} </thead> <tbody {...getTableBodyProps()}> {rows.map((row, i) => { prepareRow(row); return ( <tr {...row.getRowProps()}> {row.cells.map(cell => { return <td {...cell.getCellProps()}>{cell.render("Cell")}</td>; })} </tr> ); })} </tbody> </table> ); };
The useTable
Hook takes the column
and data
from the props, which originate from those objects and arrays that we defined earlier. We get the functions from the getTableProps
and getTableBodyProps
from the object returned from the useTable
Hook.
The getHeaderProps()
function is called inside the th
tags and spread to populate the headers. With this, we pass the props returned by the getTableBodyProps()
function to tbody
to spread the props to properly style and align the columns.
The prepareRow(row);
returned from the useTable
Hook creates the row entries, which can be automatically populated after the call to the function changes the row
object in place.
Then we have:
{rows.map((row, i) => { prepareRow(row); return ( <tr {...row.getRowProps()}> {row.cells.map(cell => { return <td {...cell.getCellProps()}>{cell.render("Cell")}</td>; })} </tr> ); })}
This automatically populates the cells by getting the items from the getCellProps()
method and then populating the values from the returned object. We called cell.render("Cell")
to render each td
as a cell.
Finally, in App
, we used the Table
component, which takes the column
and data
props. The values are the columns
and data
objects that we created earlier.
The items displayed in the table in two panes. The left pane has the Name header with two columns: First Name and Last Name. Then, the right pane has the Other Info heading with the Age column.
We can add a footer by adding a Footer
property to out column objects. We can write the following code to do that:
import React from "react"; import { useTable } from "react-table"; const data = [ { firstName: "jane", lastName: "doe", age: 20 }, { firstName: "john", lastName: "smith", age: 21 } ]; const columns = [ { Header: "Name", Footer: "Name", columns: [ { Header: "First Name", accessor: "firstName" }, { Header: "Last Name", accessor: "lastName" } ] }, { Header: "Other Info", Footer: "Other Info", columns: [ { Header: "Age", accessor: "age", Footer: info => { const total = React.useMemo( () => info.rows.reduce((sum, row) => row.values.age + sum, 0), [info.rows] ); return <>Average Age: {total / info.rows.length}</>; } } ] } ]; const Table = ({ columns, data }) => { const { getTableProps, getTableBodyProps, headerGroups, footerGroups, rows, prepareRow } = useTable({ columns, data }); return ( <table {...getTableProps()}> <thead> {headerGroups.map(headerGroup => ( <tr {...headerGroup.getHeaderGroupProps()}> {headerGroup.headers.map(column => ( <th {...column.getHeaderProps()}>{column.render("Header")}</th> ))} </tr> ))} </thead> <tbody {...getTableBodyProps()}> {rows.map((row, i) => { prepareRow(row); return ( <tr {...row.getRowProps()}> {row.cells.map(cell => { return <td {...cell.getCellProps()}>{cell.render("Cell")}</td>; })} </tr> ); })} </tbody> <tfoot> {footerGroups.map(group => ( <tr {...group.getFooterGroupProps()}> {group.headers.map(column => ( <td {...column.getFooterProps()}>{column.render("Footer")}</td> ))} </tr> ))} </tfoot> </table> ); }; export default function App() { return ( <div className="App"> <Table columns={columns} data={data} /> </div> ); }
In the code above, we added the Footer
property to the columns
array as follows:
const columns = [ { Header: "Name", Footer: "Name", columns: [ { Header: "First Name", accessor: "firstName" }, { Header: "Last Name", accessor: "lastName" } ] }, { Header: "Other Info", Footer: "Other Info", columns: [ { Header: "Age", accessor: "age", Footer: info => { const total = React.useMemo( () => info.rows.reduce((sum, row) => row.values.age + sum, 0), [info.rows] ); return <>Average Age: {total / info.rows.length}</>; } } ] } ];
We added the Footer
property to the top-level of each object.
Also, we add a function for the Footer
property in the object for the Age column.
The Footer
property in the object for the Age column is:
info => { const total = React.useMemo( () => info.rows.reduce((sum, row) => row.values.age + sum, 0), [info.rows] ); return <>Average Age: {total / info.rows.length}</>; }
It takes the info
object, which has all the table data. Then we summed by all the age
property values for each entry and divided it by info.row.length
to return the average age. This is displayed at the bottom of the table below the Age column.
The average will change as the row changes since we have [info.rows]
, which watches the rows for changing values and recomputes the value when the rows change.
We can add sorting to a table by calling a few functions. We have to pass in the useSortBy
Hook as the second argument of the useTable
Hook to get sorting capability in our table.
Then, in our JSX code, we have to pass in column.getSortByToggleProps()
to column.getHeaderProps
to get the sorting order of the columns in the rendered column.
We can check the order in which a column is sorted by using the column.isSorted
and column.isSortedDesc
to check if a column is sorted by ascending or descending order, respectively.
Also, we can add a sortType
property to the column array entries so we can specify the sort type. For instance, we can write the following code to add basic sorting to our table:
import React from "react"; import { useTable, useSortBy } from "react-table"; const data = [ { firstName: "jane", lastName: "doe" }, { firstName: "john", lastName: "smith" } ]; const columns = [ { Header: "Name", columns: [ { Header: "First Name", accessor: "firstName", sortType: "basic" }, { Header: "Last Name", accessor: "lastName", sortType: "basic" } ] } ]; const Table = ({ columns, data }) => { const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable( { columns, data }, useSortBy ); return ( <table {...getTableProps()}> <thead> {headerGroups.map(headerGroup => ( <tr {...headerGroup.getHeaderGroupProps()}> {headerGroup.headers.map(column => ( <th {...column.getHeaderProps(column.getSortByToggleProps())}> {column.render("Header")} <span> {column.isSorted ? (column.isSortedDesc ? " 🔽" : " 🔼") : ""} </span> </th> ))} </tr> ))} </thead> <tbody {...getTableBodyProps()}> {rows.map((row, i) => { prepareRow(row); return ( <tr {...row.getRowProps()}> {row.cells.map(cell => { return <td {...cell.getCellProps()}>{cell.render("Cell")}</td>; })} </tr> ); })} </tbody> </table> ); }; export default function App() { return ( <div className="App"> <Table columns={columns} data={data} /> </div> ); }
In the code above, we specified that sortType
is 'basic'
so that words are sorted alphabetically and numbers are sorted numerically.
Then we rendered the thead
by writing:
<thead> {headerGroups.map(headerGroup => ( <tr {...headerGroup.getHeaderGroupProps()}> {headerGroup.headers.map(column => ( <th {...column.getHeaderProps(column.getSortByToggleProps())}> {column.render("Header")} <span> {column.isSorted ? (column.isSortedDesc ? " 🔽" : " 🔼") : ""} </span> </th> ))} </tr> ))} </thead>
This adds icons for indicating the sort order of each column and getting the order by which the column is sorted.
After writing that code, we’ll see a sort button to the right of our column headings that we can click on to sort the columns.
Filtering is more complex than creating a simple table or sorting. We have to create a component with an input control we can use to filter our items. The input component will take the functions that are returned from the useTable
as props and call them in the inputs.
For instance, we can write the following code to do that:
import React from "react"; import { useTable, useFilters, useGlobalFilter } from "react-table"; const data = [ { firstName: "jane", lastName: "doe", age: 20 }, { firstName: "john", lastName: "smith", age: 21 } ]; const columns = [ { Header: "Name", columns: [ { Header: "First Name", accessor: "firstName", filter: "text" }, { Header: "Last Name", accessor: "lastName", filter: "text" } ] }, { Header: "Other Info", columns: [ { Header: "Age", accessor: "age", filter: "text" } ] } ]; const DefaultColumnFilter = ({ column: { filterValue, preFilteredRows, setFilter } }) => { const count = preFilteredRows.length; return ( <input value={filterValue || ""} onChange={e => { setFilter(e.target.value || undefined); }} placeholder={`Search ${count} records...`} /> ); }; const GlobalFilter = ({ preGlobalFilteredRows, globalFilter, setGlobalFilter }) => { const count = preGlobalFilteredRows && preGlobalFilteredRows.length; return ( <span> Search:{" "} <input value={globalFilter || ""} onChange={e => { setGlobalFilter(e.target.value || undefined); // Set undefined to remove the filter entirely }} placeholder={`${count} records...`} style={{ border: "0" }} /> </span> ); }; const Table = ({ columns, data }) => { const filterTypes = React.useMemo( () => ({ text: (rows, id, filterValue) => { return rows.filter(row => { const rowValue = row.values[id]; return rowValue !== undefined ? String(rowValue) .toLowerCase() .startsWith(String(filterValue).toLowerCase()) : true; }); } }), [] ); const defaultColumn = React.useMemo( () => ({ Filter: DefaultColumnFilter }), [] ); const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow, state, visibleColumns, preGlobalFilteredRows, setGlobalFilter } = useTable( { columns, data, defaultColumn, filterTypes }, useFilters, useGlobalFilter ); return ( <table {...getTableProps()}> <thead> {headerGroups.map(headerGroup => ( <tr {...headerGroup.getHeaderGroupProps()}> {headerGroup.headers.map(column => ( <th {...column.getHeaderProps()}> {column.render("Header")} <div>{column.canFilter ? column.render("Filter") : null}</div> </th> ))} </tr> ))} <tr> <th colSpan={visibleColumns.length} style={{ textAlign: "left" }} > <GlobalFilter preGlobalFilteredRows={preGlobalFilteredRows} globalFilter={state.globalFilter} setGlobalFilter={setGlobalFilter} /> </th> </tr> </thead> <tbody {...getTableBodyProps()}> {rows.map((row, i) => { prepareRow(row); return ( <tr {...row.getRowProps()}> {row.cells.map(cell => { return <td {...cell.getCellProps()}>{cell.render("Cell")}</td>; })} </tr> ); })} </tbody> </table> ); }; export default function App() { return ( <div className="App"> <Table columns={columns} data={data} /> </div> ); }
In the code above, we added the GlobalFilter
component as follows:
const GlobalFilter = ({ preGlobalFilteredRows, globalFilter, setGlobalFilter }) => { const count = preGlobalFilteredRows && preGlobalFilteredRows.length; return ( <span> Search:{" "} <input value={globalFilter || ""} onChange={e => { setGlobalFilter(e.target.value || undefined); }} placeholder={`${count} records...`} style={{ border: "0" }} /> </span> ); };
This is used to search all the columns present in the data by calling the setGlobalFilter
function that’s passed in as props. The preGlobalFilteredRows
is an array in which we can count the number of rows that we’re searching for.
Then, in the Table
component, we added the following code:
const filterTypes = React.useMemo( () => ({ text: (rows, id, filterValue) => { return rows.filter(row => { const rowValue = row.values[id]; return rowValue !== undefined ? String(rowValue) .toLowerCase() .startsWith(String(filterValue).toLowerCase()) : true; }); } }), [] ); const defaultColumn = React.useMemo( () => ({ Filter: DefaultColumnFilter }), [] ); const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow, state, visibleColumns, preGlobalFilteredRows, setGlobalFilter } = useTable( { columns, data, defaultColumn, filterTypes }, useFilters, useGlobalFilter );
The defaultColumn
has a cached object, which has the DefaultColumnFilter
set as follows:
const DefaultColumnFilter = ({ column: { filterValue, preFilteredRows, setFilter } }) => { const count = preFilteredRows.length; return ( <input value={filterValue || ""} onChange={e => { setFilter(e.target.value || undefined); }} placeholder={`Search ${count} records...`} /> ); };
The defaultColumn
caches the input component that’s used to search individual columns. We also have the filterTypes
constant, which has the cached value of the filter we used to search our table.
We have an object with the text
method, which is used to search the entries that we’re looking for as we type. In the method, we called filter
on rows
to return the items that start with the given search string, which is stored in filterValue
.
We also used more of the returned properties from the useTable
Hook and passed in more arguments to the Hook, including the useFilters
and useGlobalFilter
Hooks to let us filter by column and globally, respectively.
Also, we added the defaultColumn
and filterTypes
objects to the object in the first argument to let us set the component we’ll use to do the filtering by default. filterTypes
lets us set the value to the name of our function that we created for returning filtered data from our array of data.
In the end, we get two inputs to filter each column individually and one that can filter the items from all columns globally.
We can add pagination using the usePagination
Hook, which is passed in as the argument for the useTable
Hook.
The useTable
Hook then returns a bunch of pagination-related variables that we used to track the pagination and navigate to different pages.
To make a simple table with pagination, we can write the following code:
import React from "react"; import { useTable, usePagination } from "react-table"; const firstNames = ["jane", "john", "alex"]; const lastName = ["smith", "jones"]; const data = Array(100) .fill() .map(a => ({ firstName: firstNames[Math.floor(Math.random() * firstNames.length)], lastName: lastName[Math.floor(Math.random() * lastName.length)], age: Math.ceil(75 * Math.random()) })); const columns = [ { Header: "Name", columns: [ { Header: "First Name", accessor: "firstName" }, { Header: "Last Name", accessor: "lastName" } ] }, { Header: "Other Info", columns: [ { Header: "Age", accessor: "age" } ] } ]; const Table = ({ columns, data }) => { const { getTableProps, getTableBodyProps, headerGroups, prepareRow, page, canPreviousPage, canNextPage, pageOptions, pageCount, gotoPage, nextPage, previousPage, setPageSize, state: { pageIndex, pageSize } } = useTable( { columns, data, initialState: { pageIndex: 0 } }, usePagination ); return ( <> <table {...getTableProps()}> <thead> {headerGroups.map(headerGroup => ( <tr {...headerGroup.getHeaderGroupProps()}> {headerGroup.headers.map(column => ( <th {...column.getHeaderProps()}>{column.render("Header")}</th> ))} </tr> ))} </thead> <tbody {...getTableBodyProps()}> {page.map((row, i) => { prepareRow(row); return ( <tr {...row.getRowProps()}> {row.cells.map(cell => { return ( <td {...cell.getCellProps()}>{cell.render("Cell")}</td> ); })} </tr> ); })} </tbody> </table> <div> <button onClick={() => gotoPage(0)} disabled={!canPreviousPage}> {"<<"} </button>{" "} <button onClick={() => previousPage()} disabled={!canPreviousPage}> {"<"} </button>{" "} <button onClick={() => nextPage()} disabled={!canNextPage}> {">"} </button>{" "} <button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}> {">>"} </button>{" "} <span> Page{" "} <strong> {pageIndex + 1} of {pageOptions.length} </strong>{" "} </span> <span> | Go to page:{" "} <input type="number" defaultValue={pageIndex + 1} onChange={e => { const page = e.target.value ? Number(e.target.value) - 1 : 0; gotoPage(page); }} style={{ width: "100px" }} /> </span>{" "} <select value={pageSize} onChange={e => { setPageSize(Number(e.target.value)); }} > {[10, 20, 30, 40, 50].map(pageSize => ( <option key={pageSize} value={pageSize}> Show {pageSize} </option> ))} </select> </div> </> ); }; export default function App() { return ( <div className="App"> <Table columns={columns} data={data} /> </div> ); }
In the code above, we generated 100 array entries with random names and pages of people. The headers are the same as the simple table example above.
In the Table
component, we have:
const { getTableProps, getTableBodyProps, headerGroups, prepareRow, page, canPreviousPage, canNextPage, pageOptions, pageCount, gotoPage, nextPage, previousPage, setPageSize, state: { pageIndex, pageSize } } = useTable( { columns, data, initialState: { pageIndex: 0 } }, usePagination );
With this, we can get various pieces of data we need for pagination, like pageSize
to change the number of items displayed on each page.
canPreviousPage
and canNextPage
tell us whether we can move to the previous or next page respectively. pageCount
has the total page count, and gotoPage
is a function that lets us skip to the given page number. previousPage
and nextPage
are also functions that let us navigate to the given page.
They’re used in the following div
to navigate between pages:
<div> <button onClick={() => gotoPage(0)} disabled={!canPreviousPage}> {"<<"} </button>{" "} <button onClick={() => previousPage()} disabled={!canPreviousPage}> {"<"} </button>{" "} <button onClick={() => nextPage()} disabled={!canNextPage}> {">"} </button>{" "} <button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}> {">>"} </button>{" "} <span> Page{" "} <strong> {pageIndex + 1} of {pageOptions.length} </strong>{" "} </span> <span> | Go to page:{" "} <input type="number" defaultValue={pageIndex + 1} onChange={e => { const page = e.target.value ? Number(e.target.value) - 1 : 0; gotoPage(page); }} style={{ width: "100px" }} /> </span>{" "} <select value={pageSize} onChange={e => { setPageSize(Number(e.target.value)); }} > {[10, 20, 30, 40, 50].map(pageSize => ( <option key={pageSize} value={pageSize}> Show {pageSize} </option> ))} </select> </div>
Then we get a table with the same columns as in the example above, but with pagination buttons added. We can also use the dropdown to change the size of each page.
The react-table
package integrates with Material UI to let us create a table that follows the Material Design specification.
To install Material UI, we run:
npm install @material-ui/core
Then we can use Material UI’s table components with react-table
to create the table as follows:
import React from "react"; import { useTable } from "react-table"; import MaUTable from "@material-ui/core/Table"; import TableBody from "@material-ui/core/TableBody"; import TableCell from "@material-ui/core/TableCell"; import TableHead from "@material-ui/core/TableHead"; import TableRow from "@material-ui/core/TableRow"; const data = [ { firstName: "jane", lastName: "doe", age: 20 }, { firstName: "john", lastName: "smith", age: 21 } ]; const columns = [ { Header: "Name", columns: [ { Header: "First Name", accessor: "firstName" }, { Header: "Last Name", accessor: "lastName" } ] }, { Header: "Other Info", columns: [ { Header: "Age", accessor: "age" } ] } ]; const Table = ({ columns, data }) => { const { getTableProps, headerGroups, rows, prepareRow } = useTable({ columns, data }); return ( <MaUTable {...getTableProps()}> <TableHead> {headerGroups.map(headerGroup => ( <TableRow {...headerGroup.getHeaderGroupProps()}> {headerGroup.headers.map(column => ( <TableCell {...column.getHeaderProps()}> {column.render("Header")} </TableCell> ))} </TableRow> ))} </TableHead> <TableBody> {rows.map((row, i) => { prepareRow(row); return ( <TableRow {...row.getRowProps()}> {row.cells.map(cell => { return ( <TableCell {...cell.getCellProps()}> {cell.render("Cell")} </TableCell> ); })} </TableRow> ); })} </TableBody> </MaUTable> ); }; export default function App() { return ( <div className="App"> <Table columns={columns} data={data} /> </div> ); }
In the code above, we used the Material UI components to render the table, but the data is populated by react-table
. We called the same methods we used in the simple table example to populate the rows and columns with data.
Therefore, we get the same data and columns as the simple table example, but it adheres to Material Design instead of having no styling.
Large tables can hog a user’s CPU. With LogRocket, you can see the impact of your tables and components across all of your users. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.
LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting with metrics like client CPU load, client memory usage, and more.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your React apps — start monitoring for free.
As we can see, react-table is capable of creating tables with lots of capabilities without having to create everything from scratch ourselves.
It provides us with a Hooks-based API to create tables, which is important since some devs would like to switch to using function components with Hooks now.
There are many more examples to showcase what react-table can do on its official GitHub repo. Some examples are simplified from the examples on their official website.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
3 Replies to "What’s new in React Table v7?"
For the pagination you need to add this if statement to only show what should be on a certain page.
Under the row mapping:
if(i = pageSize * (pageIndex))
Otherwise, great tutorial. Thanks!
Could you please explain how we can implement edit , delete and add rows functionality??
Is the only way to enable line add/remove is by using “selected” or is there a simpler way?