Skip to content

Commit

Permalink
feat: Remove inferred row state and add rowEnhancer option
Browse files Browse the repository at this point in the history
A lot of the row state was actually inferred from other state. This
creates an unnecessary coupling between the selection state and the
actual render data.

To impreove this, this PR adds the `rowEnhancer` concept. This allows
users to 'enhance' their rows with extra functionality or data with
access to the table state. The enhancer runs during the creation of
`rows` which means the user does not need to unnecessarily iterate over
the return of `useTable` again.

Also makes it easier to split up the different substates in the future
  • Loading branch information
ling1726 committed Aug 11, 2022
1 parent 5855e52 commit fa0f976
Show file tree
Hide file tree
Showing 8 changed files with 93 additions and 74 deletions.
34 changes: 15 additions & 19 deletions packages/react-components/react-table/src/hooks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ export interface ColumnDefinition<TItem> {
compare?: (a: TItem, b: TItem) => number;
}

export type RowEnhancer<TItem, TRowState extends RowState<TItem> = RowState<TItem>> = (
row: RowState<TItem>,
state: { selection: SelectionStateInternal; sort: SortStateInternal<TItem> },
) => TRowState;

export interface SortStateInternal<TItem> {
sortDirection: SortDirection;
sortColumn: ColumnId | undefined;
Expand All @@ -22,11 +27,12 @@ export interface SortStateInternal<TItem> {
sort: (items: TItem[]) => TItem[];
}

export interface UseTableOptions<TItem> {
export interface UseTableOptions<TItem, TRowState extends RowState<TItem> = RowState<TItem>> {
columns: ColumnDefinition<TItem>[];
items: TItem[];
selectionMode?: 'single' | 'multiselect';
getRowId?: (item: TItem) => RowId;
rowEnhancer?: RowEnhancer<TItem, TRowState>;
}

export interface SelectionStateInternal {
Expand All @@ -35,6 +41,7 @@ export interface SelectionStateInternal {
selectRow: (rowId: RowId) => void;
toggleSelectAllRows: () => void;
toggleRowSelect: (rowId: RowId) => void;
isRowSelected: (rowId: RowId) => boolean;
selectedRows: Set<RowId>;
allRowsSelected: boolean;
someRowsSelected: boolean;
Expand Down Expand Up @@ -96,40 +103,29 @@ export interface SelectionState {
* Whether some rows are selected
*/
someRowsSelected: boolean;

/**
* Checks if a given rowId is selected
*/
isRowSelected: (rowId: RowId) => boolean;
}

export interface RowState<TItem> {
/**
* User provided data
*/
item: TItem;
/**
* Toggle the selection of the row
*/
toggleSelect: () => void;
/**
* Selects the row
*/
selectRow: () => void;
/**
* De-selects the row
*/
deSelectRow: () => void;
/**
* Whether the row is selected
*/
selected: boolean;
/**
* The row id, defaults to index position in the collection
*/
rowId: RowId;
}

export interface TableState<TItem> {
export interface TableState<TItem, TRowState extends RowState<TItem> = RowState<TItem>> {
/**
* The row data for rendering
*/
rows: RowState<TItem>[];
rows: TRowState[];
/**
* State and actions to manage row selection
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,15 @@ export function useMultipleSelection<TItem>(items: TItem[], getRowId: GetRowIdIn
setSelected(new Set<RowId>());
}, []);

const isRowSelected: SelectionStateInternal['isRowSelected'] = React.useCallback(
(rowId: RowId) => {
return selected.has(rowId);
},
[selected],
);

return {
isRowSelected,
clearSelection,
deSelectRow,
selectRow,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@ export function useSingleSelection(): SelectionStateInternal {
setSelected(undefined);
}, []);

const isRowSelected: SelectionStateInternal['isRowSelected'] = React.useCallback(
(rowId: RowId) => {
return selected === rowId;
},
[selected],
);

return {
isRowSelected,
deSelectRow: clearSelection,
clearSelection,
selectRow: toggleRowSelect,
Expand Down
55 changes: 18 additions & 37 deletions packages/react-components/react-table/src/hooks/useTable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ describe('useTable', () => {
"allRowsSelected": false,
"clearSelection": [Function],
"deSelectRow": [Function],
"isRowSelected": [Function],
"selectRow": [Function],
"selectedRows": Array [],
"someRowsSelected": false,
Expand Down Expand Up @@ -82,89 +83,69 @@ describe('useTable', () => {
`);
});

describe('rows', () => {
it('should have selectRow action', () => {
describe('rowEnhancer', () => {
it('should enahnce rows', () => {
const { result } = renderHook(() =>
useTable({
columns: [{ columnId: 1 }],
items: [{}, {}, {}],
rowEnhancer: row => ({ ...row, foo: 'bar' }),
}),
);

act(() => {
result.current.rows[1].selectRow();
});

expect(result.current.selection.selectedRows.length).toBe(1);
expect(result.current.selection.selectedRows[0]).toBe(1);
expect(result.current.rows.map(row => row.foo)).toEqual(['bar', 'bar', 'bar']);
});

it('should have deSelectRow action', () => {
it('should have access to state', () => {
const { result } = renderHook(() =>
useTable({
columns: [{ columnId: 1 }],
items: [{}, {}, {}],
rowEnhancer: (row, { selection }) => ({ ...row, selectRow: () => selection.selectRow(row.rowId) }),
}),
);

act(() => {
result.current.rows[1].selectRow();
});

act(() => {
result.current.rows[1].deSelectRow();
});

expect(result.current.selection.selectedRows.length).toBe(0);
expect(result.current.selection.isRowSelected(1));
});
});

it('should have toggleSelect action', () => {
describe('rows', () => {
it('should return position index as rowId by default', () => {
const { result } = renderHook(() =>
useTable({
columns: [{ columnId: 1 }],
items: [{}, {}, {}],
}),
);

act(() => {
result.current.rows[1].toggleSelect();
});

expect(result.current.selection.selectedRows.length).toBe(1);
expect(result.current.selection.selectedRows[0]).toBe(1);

act(() => {
result.current.rows[1].toggleSelect();
});

expect(result.current.selection.selectedRows.length).toBe(0);
expect(result.current.rows.map(row => row.rowId)).toEqual([0, 1, 2]);
});

it('should have selected status of item', () => {
it('should return original items', () => {
const { result } = renderHook(() =>
useTable({
columns: [{ columnId: 1 }],
items: [{}, {}, {}],
items: [{ value: 1 }, { value: 2 }, { value: 3 }],
}),
);

act(() => {
result.current.rows[1].selectRow();
});

expect(result.current.rows[1].selected).toBe(true);
expect(result.current.rows.map(row => row.item)).toEqual([{ value: 1 }, { value: 2 }, { value: 3 }]);
});

it('should use getRowId', () => {
it('should use custom rowId', () => {
const { result } = renderHook(() =>
useTable({
columns: [{ columnId: 1 }],
items: [{ value: 'a' }],
items: [{ value: 'a' }, { value: 'b' }, { value: 'c' }],
getRowId: item => item.value,
}),
);

expect(result.current.rows[0].rowId).toBe('a');
expect(result.current.rows.map(row => row.rowId)).toEqual(['a', 'b', 'c']);
});
});
});
34 changes: 21 additions & 13 deletions packages/react-components/react-table/src/hooks/useTable.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import * as React from 'react';
import type { UseTableOptions, TableState } from './types';
import type { UseTableOptions, TableState, RowState } from './types';
import { useSelection } from './useSelection';
import { useSort } from './useSort';

export function useTable<TItem>(options: UseTableOptions<TItem>): TableState<TItem> {
export function useTable<TItem, TRowState extends RowState<TItem> = RowState<TItem>>(
options: UseTableOptions<TItem, TRowState>,
): TableState<TItem, TRowState> {
const {
items: baseItems,
columns,
getRowId: getUserRowId = () => undefined,
selectionMode = 'multiselect',
rowEnhancer = (row: RowState<TItem>) => row as TRowState,
} = options;

const getRowId = React.useCallback((item: TItem, index: number) => getUserRowId(item) ?? index, [getUserRowId]);
const { sortColumn, sortDirection, toggleColumnSort, setColumnSort, headerSortProps, sort } = useSort(columns);
const sortState = useSort(columns);
const { sortColumn, sortDirection, toggleColumnSort, setColumnSort, headerSortProps, sort } = sortState;

const selectionState = useSelection(selectionMode, baseItems, getRowId);
const {
isRowSelected,
toggleRowSelect,
toggleSelectAllRows,
selectedRows,
Expand All @@ -23,24 +29,26 @@ export function useTable<TItem>(options: UseTableOptions<TItem>): TableState<TIt
clearSelection,
selectRow,
deSelectRow,
} = useSelection(selectionMode, baseItems, getRowId);
} = selectionState;

const rows = React.useMemo(
() =>
sort(baseItems).map((item, i) => ({
item,
deSelectRow: () => deSelectRow(getRowId(item, i)),
selectRow: () => selectRow(getRowId(item, i)),
toggleSelect: () => toggleRowSelect(getRowId(item, i)),
selected: selectedRows.has(getRowId(item, i)),
rowId: getRowId(item, i),
})),
[baseItems, selectedRows, sort, toggleRowSelect, getRowId, selectRow, deSelectRow],
sort(baseItems).map((item, i) => {
return rowEnhancer(
{
item,
rowId: getRowId(item, i),
},
{ selection: selectionState, sort: sortState },
);
}),
[baseItems, getRowId, sort, rowEnhancer, selectionState, sortState],
);

return {
rows,
selection: {
isRowSelected,
clearSelection,
deSelectRow,
selectRow,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,16 @@ const columns: ColumnDefinition<Item>[] = [
export const MultipleSelect = () => {
const {
rows,
selection: { someRowsSelected, allRowsSelected, toggleSelectAllRows },
} = useTable({ columns, items });
selection: { allRowsSelected, someRowsSelected, toggleSelectAllRows },
} = useTable({
columns,
items,
rowEnhancer: (row, { selection }) => ({
...row,
toggleSelect: () => selection.toggleRowSelect(row.rowId),
selected: selection.isRowSelected(row.rowId),
}),
});

return (
<Table sortable>
Expand All @@ -114,7 +122,7 @@ export const MultipleSelect = () => {
</TableRow>
</TableHeader>
<TableBody>
{rows.map(({ item, toggleSelect, selected }) => (
{rows.map(({ item, selected, toggleSelect }) => (
<TableRow key={item.file.label} onClick={toggleSelect} aria-selected={selected}>
<TableSelectionCell checked={selected} />
<TableCell media={item.file.icon}>{item.file.label}</TableCell>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,17 @@ const columns: ColumnDefinition<Item>[] = [
},
];

export const Selection = () => {
const { rows } = useTable({ columns, items, selectionMode: 'single' });
export const SingleSelect = () => {
const { rows } = useTable({
columns,
items,
selectionMode: 'single',
rowEnhancer: (row, { selection }) => ({
...row,
selected: selection.isRowSelected(row.rowId),
toggleSelect: () => selection.toggleRowSelect(row.rowId),
}),
});

return (
<Table sortable>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export { SizeSmall } from './SizeSmall.stories';
export { SizeSmaller } from './SizeSmaller.stories';
export { NonNativeElements } from './NonNativeElements.stories';
export { MultipleSelect } from './MultipleSelect.stories';
export { SingleSelect } from './SingleSelect.stories';

export default {
title: 'Preview Components/Table',
Expand Down

0 comments on commit fa0f976

Please sign in to comment.