Skip to content

Instantly share code, notes, and snippets.

@ianwremmel
Created May 26, 2020 16:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ianwremmel/7572fde73db1144aa42df39e1736a4e6 to your computer and use it in GitHub Desktop.
Save ianwremmel/7572fde73db1144aa42df39e1736a4e6 to your computer and use it in GitHub Desktop.
Declarative React Table
import React from 'react';
import {Account} from '../../../mocks/account';
import {Table} from '../table/table';
export type AccountListProps = {
accounts: Account[];
};
export const AccountList = ({accounts}: AccountListProps) => (
<Table
data={accounts}
// in order to bind ColumnHeader's type-argument, we need to use a render
// function rather than passing children
setup={({ColumnHeader}) => (
<>
<ColumnHeader label="User ID" name="userId" />
<ColumnHeader label="User Name" name="username" />
<ColumnHeader label="Email" name="email" />
</>
)}
/>
);
import React, {useContext} from 'react';
import {TableContext} from './table';
export type SetupProps<T extends object> = {
ColumnHeader: React.ComponentType<ColumnHeaderProps<T>>;
};
export type ColumnHeaderProps<T extends object> = {
name: keyof T;
label: string;
setup: (props: SetupProps<T>) => JSX.Element;
};
export const ColumnHeader = <T extends object>({
label,
name,
}: ColumnHeaderProps<T>) => {
const {addColumn} = useContext(TableContext);
addColumn(name, label);
return null;
};
import React, {useContext} from 'react';
import {useTable} from 'react-table';
import {TableContext} from './table';
export type TableRendererProps<P extends object> = {
data: P[];
};
export const TableRenderer = <P extends object>({
data,
}: TableRendererProps<P>) => {
const {columns} = useContext(TableContext);
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable({columns, data});
return (
// all of the props getters here include a key, but eslint can't know that.
/* eslint-disable react/jsx-key */
<>
<table {...getTableProps()} className="table table-striped">
<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) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map((cell) => (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
))}
</tr>
);
})}
</tbody>
</table>
</>
/* eslint-enable react/jsx-key */
);
};
import React from 'react';
import {Column} from 'react-table';
import {ColumnHeader, ColumnHeaderProps} from './column-header';
import {TableRenderer} from './renderer';
type KeyType = number | string | symbol;
interface TableContext {
addColumn: (key: string, label: string) => void;
columns: Column[];
}
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore - there's no useful default we can provide
export const TableContext = React.createContext<TableContext>(null);
export type RenderProps<P extends object> = {
ColumnHeader: React.ComponentType<ColumnHeaderProps<P>>;
};
export type TableProps<P extends object> = {
data: P[];
setup: (props: RenderProps<P>) => JSX.Element;
};
export const Table = <P extends object>({
data,
setup: render,
}: TableProps<P>) => {
const columns: Column<P>[] = [];
const addColumn = (key: keyof P, label: string) =>
columns.push({
Header: label,
accessor: key,
});
return (
<TableContext.Provider
value={{
addColumn,
columns,
}}
>
{/* This _might_ be cleaner if we pass render to TableRender; if render
were invoked in TableRenderer, I think we could call it without shoving it
inside JSX */}
{render({ColumnHeader})}
<TableRenderer data={data} />
</TableContext.Provider>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment