Skip to content

Commit 30ea3f6

Browse files
authored
[refactor] move SortableColumnsContext to the DataProvider (#466)
* [refactor] move SortableColumnsContext to the DataProvider * useState instaead of useMemo * [refactor] columnDescriptors -> columnNames (we only need the names) * [refactor] delete NumColumnsContext which is redundant with ColumnNamesContext * fix test * fix type and comments * remove dead code * update comments and use readonly on column names type * put stable context outside
1 parent ad925c4 commit 30ea3f6

10 files changed

Lines changed: 124 additions & 150 deletions

src/contexts/ColumnParametersContext.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,3 @@ export interface ColumnParameters extends ColumnConfig, Pick<ColumnDescriptor, '
1818
*/
1919
type ColumnParametersContextType = ColumnParameters[]
2020
export const ColumnParametersContext = createContext<ColumnParametersContextType>([])
21-
22-
/**
23-
* A set of the names of the sortable columns. Used to check if a column is sortable, and to provide the toggle function in the OrderByContext.
24-
*/
25-
type SortableColumnsContextType = Set<string>
26-
export const SortableColumnsContext = createContext<SortableColumnsContextType>(new Set())

src/contexts/DataContext.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createContext } from 'react'
22

3-
import type { ColumnDescriptor, DataFrame } from '../helpers/dataframe/types.js'
3+
import type { DataFrame } from '../helpers/dataframe/types.js'
44

55
/**
66
* The data frame, limited to the getRowNumber, getCell, and fetch methods.
@@ -10,14 +10,35 @@ import type { ColumnDescriptor, DataFrame } from '../helpers/dataframe/types.js'
1010
export type DataFrameMethods = Pick<DataFrame, 'getRowNumber' | 'getCell' | 'fetch'>
1111
export type DataFrameWithoutMethods = Omit<DataFrame, 'getRowNumber' | 'getCell' | 'fetch'>
1212

13+
/**
14+
* The version of the data frame (incremented on each update or resolve event).
15+
*/
1316
export const DataVersionContext = createContext<number>(0)
17+
/**
18+
* The number of rows in the data frame.
19+
*/
1420
export const NumRowsContext = createContext<number>(0)
15-
export const ColumnDescriptorsContext = createContext<Pick<ColumnDescriptor, 'name' | 'sortable'>[]>([])
16-
export const NumColumnsContext = createContext<number>(0)
21+
/**
22+
* The list of column names, in the order they are defined in the data frame column descriptors.
23+
*/
24+
export const ColumnNamesContext = createContext<readonly string[]>([])
25+
/**
26+
* A set of the names of the sortable columns, used to check if a column is sortable.
27+
*/
28+
export const SortableColumnsContext = createContext<ReadonlySet<string>>(new Set())
29+
/**
30+
* Whether the table is in exclusive sort mode, which means that only one column can be sorted at a time.
31+
*/
1732
export const ExclusiveSortContext = createContext<boolean>(false)
33+
/**
34+
* The data frame methods (getRowNumber, getCell, and fetch) are provided together.
35+
* They might change over time with the context staying the same.
36+
*/
1837
export const DataFrameMethodsContext = createContext<DataFrameMethods>({
1938
getRowNumber: () => undefined,
2039
getCell: () => undefined,
2140
})
22-
// the data key is only used in tests
41+
/**
42+
* A stable key for the data instance, used in tests to check if the data frame has changed.
43+
*/
2344
export const DataKeyContext = createContext<number>(0)
Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type ReactNode, useContext, useMemo } from 'react'
22

3-
import { type ColumnParameters, ColumnParametersContext, SortableColumnsContext } from '../contexts/ColumnParametersContext.js'
4-
import { ColumnDescriptorsContext } from '../contexts/DataContext.js'
3+
import { type ColumnParameters, ColumnParametersContext } from '../contexts/ColumnParametersContext.js'
4+
import { ColumnNamesContext } from '../contexts/DataContext.js'
55
import type { HighTableProps } from '../types.js'
66

77
type Props = Pick<HighTableProps, 'columnConfiguration'> & {
@@ -12,21 +12,16 @@ type Props = Pick<HighTableProps, 'columnConfiguration'> & {
1212
/**
1313
* Provide the columns configuration to the table, through the ColumnParametersContext.
1414
*
15-
* It merges the column descriptors from the data frame with the user-provided configuration.
15+
* It merges the column names with the user-provided configuration.
1616
*/
1717
export function ColumnParametersProvider({ columnConfiguration, children }: Props) {
18-
const columnDescriptors = useContext(ColumnDescriptorsContext)
19-
20-
// A column is sortable iif it's marked as sortable in the column descriptors from the data frame. The user configuration can't change that.
21-
const sortableColumns = useMemo(() => {
22-
return new Set(columnDescriptors.filter(({ sortable }) => sortable).map(({ name }) => name))
23-
}, [columnDescriptors])
18+
const columnNames = useContext(ColumnNamesContext)
2419

2520
const columnParameters = useMemo(() => {
26-
const inHeader = new Set(columnDescriptors.map(c => c.name))
21+
const inHeader = new Set(columnNames)
2722

28-
// Build descriptors following DataFrame columns order
29-
const cols: ColumnParameters[] = columnDescriptors.map(({ name }, index) => ({
23+
// Build column parameters following column names order
24+
const cols: ColumnParameters[] = columnNames.map((name, index) => ({
3025
name,
3126
index,
3227
...columnConfiguration?.[name] ?? {},
@@ -43,13 +38,11 @@ export function ColumnParametersProvider({ columnConfiguration, children }: Prop
4338
}
4439

4540
return cols
46-
}, [columnDescriptors, columnConfiguration])
41+
}, [columnNames, columnConfiguration])
4742

4843
return (
4944
<ColumnParametersContext.Provider value={columnParameters}>
50-
<SortableColumnsContext.Provider value={sortableColumns}>
51-
{children}
52-
</SortableColumnsContext.Provider>
45+
{children}
5346
</ColumnParametersContext.Provider>
5447
)
5548
}

src/providers/ColumnWidthsProvider.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'r
33

44
import { ColumnParametersContext } from '../contexts/ColumnParametersContext.js'
55
import { ColumnWidthsContext } from '../contexts/ColumnWidthsContext.js'
6-
import { NumColumnsContext } from '../contexts/DataContext.js'
6+
import { ColumnNamesContext } from '../contexts/DataContext.js'
77
import { TableCornerWidthContext } from '../contexts/TableCornerSizeContext.js'
88
import { ViewportWidthContext } from '../contexts/ViewportSizeContext.js'
99
import { cellStyle } from '../helpers/width.js'
@@ -77,12 +77,8 @@ export function ColumnWidthsProvider({ children, localStorageKey, minWidth }: Co
7777
/** Current table corner width (used to compute the maximum total width) */
7878
const tableCornerWidth = useContext(TableCornerWidthContext)
7979
/** Number of columns (used to initialize the widths array, and compute the widths) */
80-
const numColumns = useContext(NumColumnsContext)
80+
const numColumns = useContext(ColumnNamesContext).length
8181

82-
// Number of columns
83-
if (!Number.isInteger(numColumns) || numColumns < 0) {
84-
throw new Error(`Invalid numColumns: ${numColumns}. It must be a positive integer.`)
85-
}
8682
const isValidIndex = useCallback((index?: number): index is number => {
8783
return index !== undefined && Number.isInteger(index) && index >= 0 && index < numColumns
8884
}, [numColumns])

src/providers/DataProvider.tsx

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type ReactNode, useEffect, useState } from 'react'
22

33
import type { DataFrameWithoutMethods } from '../contexts/DataContext.js'
4-
import { ColumnDescriptorsContext, DataFrameMethodsContext, DataKeyContext, DataVersionContext, ExclusiveSortContext, NumColumnsContext, NumRowsContext } from '../contexts/DataContext.js'
4+
import { ColumnNamesContext, DataFrameMethodsContext, DataKeyContext, DataVersionContext, ExclusiveSortContext, NumRowsContext, SortableColumnsContext } from '../contexts/DataContext.js'
55
import type { HighTableProps } from '../types.js'
66

77
// Assign stable numeric ids to data instances without triggering state
@@ -23,9 +23,15 @@ type Props = Pick<HighTableProps, 'data'> & {
2323
}
2424

2525
/**
26-
* Provides the number of rows and columns, and the version of the data frame.
26+
* Provides the data frame parts as separate contexts:
27+
* - number of rows
28+
* - version of the data frame (incremented on each update or resolve event)
29+
* - column names
30+
* - sortable columns
31+
* - exclusive sort flag
32+
* - getRowNumber, getCell, and fetch methods
2733
*
28-
* It also providers a data key for testing purposes.
34+
* It also provides a data key for testing purposes.
2935
*/
3036
export function DataProvider({ children, data }: Props) {
3137
const key = getDataKey(data)
@@ -63,9 +69,9 @@ function KeyedDataProvider({ children, data }: KeyedDataProviderProps) {
6369

6470
// Some dataframe properties are expected to be stable for a given data frame.
6571
// We keep their initial value, no setter.
66-
const [columnDescriptors] = useState(() => data.columnDescriptors.map(({ name, sortable }) => ({ name, sortable })))
67-
const numColumns = columnDescriptors.length
6872
const [exclusiveSort] = useState(() => data.exclusiveSort === true)
73+
const [columnNames] = useState(() => data.columnDescriptors.map(({ name }) => name))
74+
const [sortableColumns] = useState(() => new Set(data.columnDescriptors.filter(({ sortable }) => sortable).map(({ name }) => name)))
6975

7076
// Synchronize version and numRows with data frame events (external system - useEffect is needed)
7177
useEffect(() => {
@@ -87,16 +93,16 @@ function KeyedDataProvider({ children, data }: KeyedDataProviderProps) {
8793

8894
// Multiple contexts, to avoid unnecessary re-renders of the components consuming the API when only the data changes, and vice-versa. See https://react.dev/reference/react/useContext#caveats for more details.
8995
return (
90-
<DataVersionContext.Provider value={version}>
91-
<ColumnDescriptorsContext.Provider value={columnDescriptors}>
92-
<NumColumnsContext.Provider value={numColumns}>
93-
<ExclusiveSortContext.Provider value={exclusiveSort}>
96+
<ColumnNamesContext.Provider value={columnNames}>
97+
<SortableColumnsContext.Provider value={sortableColumns}>
98+
<ExclusiveSortContext.Provider value={exclusiveSort}>
99+
<DataVersionContext.Provider value={version}>
94100
<NumRowsContext.Provider value={numRows}>
95101
{children}
96102
</NumRowsContext.Provider>
97-
</ExclusiveSortContext.Provider>
98-
</NumColumnsContext.Provider>
99-
</ColumnDescriptorsContext.Provider>
100-
</DataVersionContext.Provider>
103+
</DataVersionContext.Provider>
104+
</ExclusiveSortContext.Provider>
105+
</SortableColumnsContext.Provider>
106+
</ColumnNamesContext.Provider>
101107
)
102108
}

src/providers/OrderByProvider.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { type ReactNode, useContext, useMemo } from 'react'
22

3-
import { SortableColumnsContext } from '../contexts/ColumnParametersContext.js'
4-
import { ExclusiveSortContext } from '../contexts/DataContext.js'
3+
import { ExclusiveSortContext, SortableColumnsContext } from '../contexts/DataContext.js'
54
import { OrderByContext, SortInfoAndActionsByColumnContext } from '../contexts/OrderByContext.js'
65
import { type OrderBy, toggleColumn, toggleColumnExclusive } from '../helpers/sort.js'
76
import { useInputState } from '../hooks/useInputState.js'

test/components/ColumnHeader.test.tsx

Lines changed: 30 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { act, fireEvent } from '@testing-library/react'
22
import { beforeEach, describe, expect, it, vi } from 'vitest'
33

44
import ColumnHeader from '../../src/components/ColumnHeader.js'
5-
import { ColumnDescriptorsContext, NumColumnsContext } from '../../src/contexts/DataContext.js'
5+
import { ColumnNamesContext } from '../../src/contexts/DataContext.js'
66
import { SortInfoAndActionsByColumnContext } from '../../src/contexts/OrderByContext.js'
77
import { getOffsetWidth } from '../../src/helpers/width.js'
88
import { ColumnParametersProvider } from '../../src/providers/ColumnParametersProvider.js'
@@ -62,11 +62,11 @@ describe('ColumnHeader', () => {
6262

6363
it('measures the width if canMeasureWidth is true', () => {
6464
render(
65-
<NumColumnsContext.Provider value={1}>
65+
<ColumnNamesContext.Provider value={['test']}>
6666
<ColumnWidthsProvider minWidth={10}>
6767
<table><thead><tr><ColumnHeader columnName="test" {...defaultProps} canMeasureWidth={true} /></tr></thead></table>
6868
</ColumnWidthsProvider>
69-
</NumColumnsContext.Provider>
69+
</ColumnNamesContext.Provider>
7070
)
7171
expect(getOffsetWidth).toHaveBeenCalled()
7272
})
@@ -95,11 +95,11 @@ describe('ColumnHeader', () => {
9595
localStorage.setItem(cacheKey, JSON.stringify([savedWidth]))
9696

9797
const { getByRole } = render(
98-
<NumColumnsContext.Provider value={1}>
98+
<ColumnNamesContext.Provider value={['test']}>
9999
<ColumnWidthsProvider localStorageKey={cacheKey} minWidth={10}>
100100
<table><thead><tr><ColumnHeader columnName="test" {...defaultProps} /></tr></thead></table>
101101
</ColumnWidthsProvider>
102-
</NumColumnsContext.Provider>
102+
</ColumnNamesContext.Provider>
103103
)
104104
const header = getByRole('columnheader')
105105
expect(header.style.maxWidth).toEqual(`${savedWidth}px`)
@@ -113,11 +113,11 @@ describe('ColumnHeader', () => {
113113
localStorage.setItem(cacheKey, JSON.stringify([savedWidth]))
114114

115115
const { getByRole } = render(
116-
<NumColumnsContext.Provider value={1}>
116+
<ColumnNamesContext.Provider value={['test']}>
117117
<ColumnWidthsProvider localStorageKey={cacheKey} minWidth={minWidth}>
118118
<table><thead><tr><ColumnHeader columnName="test" {...defaultProps} /></tr></thead></table>
119119
</ColumnWidthsProvider>
120-
</NumColumnsContext.Provider>
120+
</ColumnNamesContext.Provider>
121121
)
122122
const header = getByRole('columnheader')
123123
expect(header.style.maxWidth).toEqual(expected)
@@ -130,11 +130,11 @@ describe('ColumnHeader', () => {
130130
localStorage.setItem(cacheKey, JSON.stringify([savedWidth]))
131131

132132
const { user, getByRole } = render(
133-
<NumColumnsContext.Provider value={1}>
133+
<ColumnNamesContext.Provider value={['test']}>
134134
<ColumnWidthsProvider localStorageKey={cacheKey} minWidth={minWidth}>
135135
<table><thead><tr><ColumnHeader columnName="test" canMeasureWidth={true} {...defaultProps} /></tr></thead></table>
136136
</ColumnWidthsProvider>
137-
</NumColumnsContext.Provider>
137+
</ColumnNamesContext.Provider>
138138
)
139139
const header = getByRole('columnheader')
140140
const resizeHandle = getByRole('spinbutton')
@@ -156,11 +156,11 @@ describe('ColumnHeader', () => {
156156
localStorage.setItem(cacheKey, JSON.stringify([savedWidth]))
157157

158158
const { user, getByRole } = render(
159-
<NumColumnsContext.Provider value={1}>
159+
<ColumnNamesContext.Provider value={['test']}>
160160
<ColumnWidthsProvider localStorageKey={cacheKey} minWidth={10}>
161161
<table><thead><tr><ColumnHeader columnName="test" {...defaultProps} /></tr></thead></table>
162162
</ColumnWidthsProvider>
163-
</NumColumnsContext.Provider>
163+
</ColumnNamesContext.Provider>
164164
)
165165

166166
// Simulate resizing the column
@@ -187,18 +187,15 @@ describe('ColumnHeader', () => {
187187
localStorage.setItem(cacheKey, JSON.stringify([savedWidth]))
188188

189189
const columnConfiguration = { test: { minWidth: columnMinWidth } }
190-
const columnDescriptors = [{ name: 'test' }]
191190

192191
const { user, getByRole } = render(
193-
<ColumnDescriptorsContext.Provider value={columnDescriptors}>
194-
<NumColumnsContext.Provider value={1}>
195-
<ColumnParametersProvider columnConfiguration={columnConfiguration}>
196-
<ColumnWidthsProvider localStorageKey={cacheKey} minWidth={10}>
197-
<table><thead><tr><ColumnHeader columnName="test" canMeasureWidth={true} {...defaultProps} /></tr></thead></table>
198-
</ColumnWidthsProvider>
199-
</ColumnParametersProvider>
200-
</NumColumnsContext.Provider>
201-
</ColumnDescriptorsContext.Provider>
192+
<ColumnNamesContext.Provider value={['test']}>
193+
<ColumnParametersProvider columnConfiguration={columnConfiguration}>
194+
<ColumnWidthsProvider localStorageKey={cacheKey} minWidth={10}>
195+
<table><thead><tr><ColumnHeader columnName="test" canMeasureWidth={true} {...defaultProps} /></tr></thead></table>
196+
</ColumnWidthsProvider>
197+
</ColumnParametersProvider>
198+
</ColumnNamesContext.Provider>
202199
)
203200

204201
const header = getByRole('columnheader')
@@ -225,18 +222,15 @@ describe('ColumnHeader', () => {
225222
localStorage.setItem(cacheKey, JSON.stringify([savedWidth]))
226223

227224
const columnConfiguration = { test: { minWidth: columnMinWidth } }
228-
const columnDescriptors = [{ name: 'test' }]
229225

230226
const { user, getByRole } = render(
231-
<ColumnDescriptorsContext.Provider value={columnDescriptors}>
232-
<NumColumnsContext.Provider value={1}>
233-
<ColumnParametersProvider columnConfiguration={columnConfiguration}>
234-
<ColumnWidthsProvider localStorageKey={cacheKey} minWidth={globalMinWidth}>
235-
<table><thead><tr><ColumnHeader columnName="test" canMeasureWidth={true} {...defaultProps} /></tr></thead></table>
236-
</ColumnWidthsProvider>
237-
</ColumnParametersProvider>
238-
</NumColumnsContext.Provider>
239-
</ColumnDescriptorsContext.Provider>
227+
<ColumnNamesContext.Provider value={['test']}>
228+
<ColumnParametersProvider columnConfiguration={columnConfiguration}>
229+
<ColumnWidthsProvider localStorageKey={cacheKey} minWidth={globalMinWidth}>
230+
<table><thead><tr><ColumnHeader columnName="test" canMeasureWidth={true} {...defaultProps} /></tr></thead></table>
231+
</ColumnWidthsProvider>
232+
</ColumnParametersProvider>
233+
</ColumnNamesContext.Provider>
240234
)
241235

242236
const header = getByRole('columnheader')
@@ -262,21 +256,22 @@ describe('ColumnHeader', () => {
262256
const width2 = 300
263257
localStorage.setItem(cacheKey2, JSON.stringify([width2]))
264258

259+
const columnNames = ['test']
265260
const { rerender, getByRole } = render(
266-
<NumColumnsContext.Provider value={1}>
261+
<ColumnNamesContext.Provider value={columnNames}>
267262
<ColumnWidthsProvider localStorageKey={cacheKey} minWidth={10}>
268263
<table><thead><tr><ColumnHeader columnName="test" {...defaultProps} /></tr></thead></table>
269264
</ColumnWidthsProvider>
270-
</NumColumnsContext.Provider>
265+
</ColumnNamesContext.Provider>
271266
)
272267
const header = getByRole('columnheader')
273268
expect(header.style.maxWidth).toEqual(`${width1}px`)
274269
rerender(
275-
<NumColumnsContext.Provider value={1}>
270+
<ColumnNamesContext.Provider value={columnNames}>
276271
<ColumnWidthsProvider localStorageKey={cacheKey2} minWidth={10}>
277272
<table><thead><tr><ColumnHeader columnName="test" {...defaultProps} /></tr></thead></table>
278273
</ColumnWidthsProvider>
279-
</NumColumnsContext.Provider>
274+
</ColumnNamesContext.Provider>
280275
)
281276
expect(header.style.maxWidth).toEqual(`${width2}px`)
282277
})

0 commit comments

Comments
 (0)