Skip to content

Commit 89d5fb9

Browse files
authored
[refactor] use contexts with no intermediate hooks, and move logic to providers (#463)
* [refactor] use contexts with no intermediate hooks, and move logic to providers * add a test about data frame methods * [refactor] rename DataContext to the ugly but explicit DataFrameMethodsContext * add a test to show that methods can be updated later * fix mock function * don't hardcode the default row height
1 parent 140ac1e commit 89d5fb9

20 files changed

Lines changed: 179 additions & 167 deletions

src/components/HighTable/Scroller.tsx

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

44
import { CellNavigationContext } from '../../contexts/CellNavigationContext.js'
55
import { ScrollContext } from '../../contexts/ScrollContext.js'
6-
import { useSetViewportSize } from '../../contexts/ViewportSizeContext.js'
6+
import { SetViewportSizeContext } from '../../contexts/ViewportSizeContext.js'
77
import styles from '../../HighTable.module.css'
88

99
interface Props {
@@ -13,7 +13,7 @@ interface Props {
1313

1414
export default function Scroller({ children }: Props) {
1515
/** Callback to set the current viewport size */
16-
const setViewportSize = useSetViewportSize()
16+
const setViewportSize = useContext(SetViewportSizeContext)
1717
const { goToCurrentCell } = useContext(CellNavigationContext)
1818
const { canvasHeight, sliceTop, setScrollTop, setScrollTo } = useContext(ScrollContext)
1919

src/components/HighTable/Slice.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useCallback, useContext, useMemo } from 'react'
33

44
import { CellNavigationContext } from '../../contexts/CellNavigationContext.js'
55
import { ColumnsVisibilityContext } from '../../contexts/ColumnsVisibilityContext.js'
6-
import { useData, useDataVersion, useNumRows } from '../../contexts/DataContext.js'
6+
import { DataFrameMethodsContext, DataVersionContext, NumRowsContext } from '../../contexts/DataContext.js'
77
import { OrderByContext } from '../../contexts/OrderByContext.js'
88
import { ScrollContext } from '../../contexts/ScrollContext.js'
99
import { SelectionContext } from '../../contexts/SelectionContext.js'
@@ -34,10 +34,10 @@ export default function Slice({
3434
const { visibleColumnsParameters: columnsParameters } = useContext(ColumnsVisibilityContext)
3535
const { renderedRowsStart, renderedRowsEnd } = useContext(ScrollContext)
3636
/** A version number that increments whenever a data frame is updated or resolved (the key remains the same). */
37-
const version = useDataVersion()
37+
const version = useContext(DataVersionContext)
3838
/** The actual number of rows in the data frame */
39-
const numRows = useNumRows()
40-
const data = useData()
39+
const numRows = useContext(NumRowsContext)
40+
const dataFrameMethods = useContext(DataFrameMethodsContext)
4141

4242
// Fetch the required cells if needed (visible + overscan)
4343
// it's a side-effect.
@@ -144,9 +144,9 @@ export default function Slice({
144144

145145
const canMeasureColumn: Record<string, boolean> = {}
146146
const rowContents = rows.map((row) => {
147-
const rowNumber = data.getRowNumber({ row, orderBy })?.value
147+
const rowNumber = dataFrameMethods.getRowNumber({ row, orderBy })?.value
148148
const cells = (columnsParameters ?? []).map(({ name: column, index: originalColumnIndex, className }) => {
149-
const cell = data.getCell({ row, column, orderBy })
149+
const cell = dataFrameMethods.getCell({ row, column, orderBy })
150150
canMeasureColumn[column] ||= cell !== undefined
151151
return { columnIndex: originalColumnIndex, cell, className }
152152
})
@@ -161,7 +161,7 @@ export default function Slice({
161161
canMeasureColumn,
162162
version,
163163
}
164-
}, [data, columnsParameters, renderedRowsStart, renderedRowsEnd, orderBy, version])
164+
}, [dataFrameMethods, columnsParameters, renderedRowsStart, renderedRowsEnd, orderBy, version])
165165

166166
// don't render table if the data frame has no visible columns
167167
// (it can have zero rows, but must have at least one visible column)

src/components/HighTable/Wrapper.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import type { CSSProperties, ReactNode } from 'react'
1+
import { type CSSProperties, type ReactNode, useContext } from 'react'
22

3-
import { useNumRows } from '../../contexts/DataContext.js'
3+
import { NumRowsContext } from '../../contexts/DataContext.js'
44
import { PortalContainerContext } from '../../contexts/PortalContainerContext.js'
5-
import { useHeaderHeight } from '../../contexts/TableCornerSizeContext.js'
5+
import { TableCornerHeightContext } from '../../contexts/TableCornerSizeContext.js'
66
import styles from '../../HighTable.module.css'
77
import { useHTMLElement } from '../../hooks/useHTMLElement.js'
88
import type { HighTableProps } from '../../types.js'
@@ -14,9 +14,9 @@ type Props = Pick<HighTableProps, 'className' | 'maxRowNumber' | 'styled'> & {
1414

1515
export default function Wrapper({ children, className, maxRowNumber, styled }: Props) {
1616
/** Number of rows in the data frame */
17-
const numRows = useNumRows()
17+
const numRows = useContext(NumRowsContext)
1818
/** Height of the header, used to set a CSS variable for row height calculation in the cells */
19-
const headerHeight = useHeaderHeight()
19+
const headerHeight = useContext(TableCornerHeightContext)
2020

2121
// reserve space for at least 3 characters
2222
const numCharacters = Math.max((maxRowNumber ?? numRows).toLocaleString('en-US').length, 3)

src/components/TableCorner/TableCorner.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ChangeEvent, CSSProperties, KeyboardEvent, ReactNode } from 'react'
2-
import { useCallback, useEffect, useRef } from 'react'
2+
import { useCallback, useContext, useEffect, useRef } from 'react'
33

4-
import { useSetTableCornerSize } from '../../contexts/TableCornerSizeContext.js'
4+
import { SetTableCornerSizeContext } from '../../contexts/TableCornerSizeContext.js'
55
import { useCellFocus } from '../../hooks/useCellFocus.js'
66

77
interface Props {
@@ -16,7 +16,7 @@ interface Props {
1616

1717
export default function TableCorner({ children, checked, onCheckboxPress, pendingSelectionGesture, style, ariaColIndex, ariaRowIndex }: Props) {
1818
const { tabIndex, navigateToCell, focusIfNeeded } = useCellFocus({ ariaColIndex, ariaRowIndex })
19-
const setTableCornerSize = useSetTableCornerSize()
19+
const setTableCornerSize = useContext(SetTableCornerSizeContext)
2020

2121
// Focus the cell if needed. We use an effect, as it acts on the DOM element after render.
2222
const ref = useRef<HTMLTableCellElement | null>(null)

src/contexts/DataContext.ts

Lines changed: 11 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,23 @@
1-
import { createContext, useContext } from 'react'
1+
import { createContext } from 'react'
22

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

5+
/**
6+
* The data frame, limited to the getRowNumber, getCell, and fetch methods.
7+
*
8+
* The methods might change over time, without the data frame instance changing.
9+
*/
510
export type DataFrameMethods = Pick<DataFrame, 'getRowNumber' | 'getCell' | 'fetch'>
611
export type DataFrameWithoutMethods = Omit<DataFrame, 'getRowNumber' | 'getCell' | 'fetch'>
712

8-
export const DataKeyContext = createContext<number>(0)
913
export const DataVersionContext = createContext<number>(0)
1014
export const NumRowsContext = createContext<number>(0)
1115
export const ColumnDescriptorsContext = createContext<Pick<ColumnDescriptor, 'name' | 'sortable'>[]>([])
1216
export const NumColumnsContext = createContext<number>(0)
1317
export const ExclusiveSortContext = createContext<boolean>(false)
14-
export const DataContext = createContext<DataFrameMethods | undefined>(undefined)
15-
18+
export const DataFrameMethodsContext = createContext<DataFrameMethods>({
19+
getRowNumber: () => undefined,
20+
getCell: () => undefined,
21+
})
1622
// the data key is only used in tests
17-
export function useDataKey() {
18-
return useContext(DataKeyContext)
19-
}
20-
21-
export function useDataVersion() {
22-
return useContext(DataVersionContext)
23-
}
24-
25-
export function useNumRows() {
26-
return useContext(NumRowsContext)
27-
}
28-
29-
export function useColumnDescriptors() {
30-
return useContext(ColumnDescriptorsContext)
31-
}
32-
33-
export function useNumColumns() {
34-
return useContext(NumColumnsContext)
35-
}
36-
37-
export function useExclusiveSort() {
38-
return useContext(ExclusiveSortContext)
39-
}
40-
41-
export function useData(): DataFrameMethods {
42-
const data = useContext(DataContext)
43-
if (data === undefined) {
44-
throw new Error('useData must be used within a DataContext.Provider with a valid DataFrameMethods value')
45-
}
46-
return data
47-
}
23+
export const DataKeyContext = createContext<number>(0)
Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,10 @@
1-
import { createContext, useContext } from 'react'
1+
import { createContext } from 'react'
22

33
import { rowHeight } from '../helpers/constants.js'
44

55
type SetTableCornerSizeContextType = (element: HTMLElement) => void
66

7-
export const TableCornerHeightContext = createContext<number | undefined>(undefined)
7+
export const defaultTableCornerHeight = rowHeight
8+
export const TableCornerHeightContext = createContext<number>(defaultTableCornerHeight)
89
export const TableCornerWidthContext = createContext<number | undefined>(undefined)
910
export const SetTableCornerSizeContext = createContext<SetTableCornerSizeContextType | undefined>(undefined)
10-
11-
export function useTableCornerWidth() {
12-
return useContext(TableCornerWidthContext)
13-
}
14-
15-
export function useHeaderHeight() {
16-
return useContext(TableCornerHeightContext) ?? rowHeight
17-
}
18-
19-
export function useSetTableCornerSize() {
20-
return useContext(SetTableCornerSizeContext)
21-
}
Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,7 @@
1-
import { createContext, useContext } from 'react'
1+
import { createContext } from 'react'
22

33
type SetViewportSizeContextType = (element: HTMLElement) => void
44

55
export const ViewportHeightContext = createContext<number | undefined>(undefined)
66
export const ViewportWidthContext = createContext<number | undefined>(undefined)
77
export const SetViewportSizeContext = createContext<SetViewportSizeContextType | undefined>(undefined)
8-
9-
export function useViewportWidth() {
10-
return useContext(ViewportWidthContext)
11-
}
12-
13-
export function useViewportHeight() {
14-
return useContext(ViewportHeightContext)
15-
}
16-
17-
export function useSetViewportSize() {
18-
return useContext(SetViewportSizeContext)
19-
}

src/hooks/useFetchCells.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useContext, useEffect, useEffectEvent, useMemo } from 'react'
22

33
import { ColumnsVisibilityContext } from '../contexts/ColumnsVisibilityContext.js'
4-
import { useData, useNumRows } from '../contexts/DataContext.js'
4+
import { DataFrameMethodsContext, NumRowsContext } from '../contexts/DataContext.js'
55
import { OrderByContext } from '../contexts/OrderByContext.js'
66
import { ScrollContext } from '../contexts/ScrollContext.js'
77
import { defaultOverscan } from '../helpers/constants.js'
@@ -16,8 +16,8 @@ export function useFetchCells({ overscan = defaultOverscan, onError }: Props) {
1616
const { visibleRowsStart, visibleRowsEnd } = useContext(ScrollContext)
1717
const { visibleColumnsParameters } = useContext(ColumnsVisibilityContext)
1818
const orderBy = useContext(OrderByContext)
19-
const data = useData()
20-
const numRows = useNumRows()
19+
const dataFrameMethods = useContext(DataFrameMethodsContext)
20+
const numRows = useContext(NumRowsContext)
2121

2222
const fetchedRowsStart = useMemo(() => {
2323
if (visibleRowsStart === undefined) return undefined
@@ -44,13 +44,13 @@ export function useFetchCells({ overscan = defaultOverscan, onError }: Props) {
4444
// Keep this inside an effect so we don't update state
4545
// or perform side-effects during render, for example when calling onError.
4646
useEffect(() => {
47-
if (data.fetch === undefined || fetchedRowsStart === undefined || fetchedRowsEnd === undefined) return
47+
if (dataFrameMethods.fetch === undefined || fetchedRowsStart === undefined || fetchedRowsEnd === undefined) return
4848

4949
// Create an AbortController per fetch and clean it up on dependency changes.
5050
const abortController = new AbortController()
5151

5252
// Launch the data fetch. The promise is not awaited here, but it will be aborted if any dependency changes.
53-
data.fetch({
53+
dataFrameMethods.fetch({
5454
rowStart: fetchedRowsStart,
5555
rowEnd: fetchedRowsEnd,
5656
columns: columnNames,
@@ -66,5 +66,5 @@ export function useFetchCells({ overscan = defaultOverscan, onError }: Props) {
6666
return () => {
6767
abortController.abort()
6868
}
69-
}, [data, fetchedRowsStart, fetchedRowsEnd, columnNames, orderBy])
69+
}, [dataFrameMethods, fetchedRowsStart, fetchedRowsEnd, columnNames, orderBy])
7070
}

src/providers/CellNavigationProvider.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useCallback, useContext, useEffect, useMemo, useReducer } from 'react'
44
import type { FocusAction, FocusState, MoveCellAction } from '../contexts/CellNavigationContext.js'
55
import { CellNavigationContext } from '../contexts/CellNavigationContext.js'
66
import { ColumnsVisibilityContext } from '../contexts/ColumnsVisibilityContext.js'
7-
import { useNumRows } from '../contexts/DataContext.js'
7+
import { NumRowsContext } from '../contexts/DataContext.js'
88
import { defaultNumRowsPerPage } from '../helpers/constants.js'
99
import { useInputState } from '../hooks/useInputState.js'
1010
import type { HighTableProps } from '../types.js'
@@ -55,7 +55,7 @@ export function CellNavigationProvider({
5555
}: CellNavigationProviderProps) {
5656
const [focusState, focusDispatch] = useReducer(reducer, focus, initializeFocusState)
5757
/** The actual number of rows in the data frame */
58-
const numDataRows = useNumRows()
58+
const numDataRows = useContext(NumRowsContext)
5959

6060
const notifyChange = useCallback(() => {
6161
focusDispatch({ type: 'START' })

src/providers/ColumnParametersProvider.tsx

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

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

77
type Props = Pick<HighTableProps, 'columnConfiguration'> & {
@@ -15,7 +15,7 @@ type Props = Pick<HighTableProps, 'columnConfiguration'> & {
1515
* It merges the column descriptors from the data frame with the user-provided configuration.
1616
*/
1717
export function ColumnParametersProvider({ columnConfiguration, children }: Props) {
18-
const columnDescriptors = useColumnDescriptors()
18+
const columnDescriptors = useContext(ColumnDescriptorsContext)
1919

2020
// 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.
2121
const sortableColumns = useMemo(() => {

0 commit comments

Comments
 (0)