Skip to content

Commit 4e68731

Browse files
committed
Add basic react-data-list implementation
1 parent 861e86e commit 4e68731

20 files changed

+520
-9
lines changed

.cursor/mcp.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"mcpServers": {
3+
"RadonAi": {
4+
"url": "http://127.0.0.1:49646/mcp",
5+
"type": "http",
6+
"headers": {
7+
"nonce": "cad45732-0cce-4c49-b993-f85893b2c4ee"
8+
}
9+
}
10+
}
11+
}

biome.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,11 @@
4949
{"type": true, "source": [":BUN:", ":NODE:"]},
5050
{"type": false, "source": [":BUN:", ":NODE:"]},
5151
":BLANK_LINE:",
52-
{"type": true, "source": [":PACKAGE:"]},
53-
{"type": false, "source": [":PACKAGE:"]},
52+
{"type": true, "source": [":PACKAGE:", "!@attio/**"]},
53+
{"type": false, "source": [":PACKAGE:", "!@attio/**"]},
54+
":BLANK_LINE:",
55+
{"type": true, "source": "@attio/**"},
56+
{"type": false, "source": "@attio/**"},
5457
":BLANK_LINE:",
5558
{"type": true, "source": ":PATH:"},
5659
{"type": false, "source": ":PATH:"},

bun.lock

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/tsconfig.base.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"compilerOptions": {
55
// Language & Standard Library
66
"composite": true,
7-
"lib": [],
7+
"lib": ["ES2020"],
88
"alwaysStrict": true,
99
"useDefineForClassFields": true,
1010

example/bun.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

example/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"expo-web-browser": "~14.2.0",
3636
"react": "19.0.0",
3737
"react-dom": "19.0.0",
38+
"react-error-boundary": "^6.0.0",
3839
"react-native": "0.79.6",
3940
"react-native-gesture-handler": "~2.24.0",
4041
"react-native-reanimated": "~3.17.4",

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,10 @@
4949
"typescript": "~5.9.2"
5050
},
5151
"peerDependencies": {
52+
"@attio/react-descendants": "link:@attio/react-descendants",
5253
"@types/react": "*",
53-
"react": "*"
54+
"react": "*",
55+
"react-error-boundary": "^6.0.0"
5456
},
5557
"trustedDependencies": [
5658
"@biomejs/biome"
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React from "react"
2+
3+
import {createDescendantContext} from "@attio/react-descendants"
4+
5+
import type {DataListDescriptor, DataListDescriptorDescendant} from "./data-list-types"
6+
7+
export interface DataListDescriptorContextProviderContext {
8+
attachDescriptors: (index: number, descriptors: Array<DataListDescriptor>) => () => void
9+
}
10+
11+
const DataListDescriptorContext = React.createContext<
12+
DataListDescriptorContextProviderContext | undefined
13+
>(undefined)
14+
15+
export function DataListDescriptorContextProvider({
16+
children,
17+
...value
18+
}: DataListDescriptorContextProviderContext & {children: React.ReactNode}) {
19+
return (
20+
<DataListDescriptorContext.Provider value={value}>
21+
{children}
22+
</DataListDescriptorContext.Provider>
23+
)
24+
}
25+
26+
export function useDataListDescriptorContext(): DataListDescriptorContextProviderContext {
27+
const context = React.useContext(DataListDescriptorContext)
28+
if (context === undefined) {
29+
throw new Error(
30+
"useDataListDescriptorContext must be used within a DataListDescriptorContextProvider"
31+
)
32+
}
33+
return context
34+
}
35+
36+
interface DataListDescriptorDescendantContextProviderProps extends React.PropsWithChildren {}
37+
38+
const {DescendantsContextProvider, useDescendant} =
39+
createDescendantContext<DataListDescriptorDescendant>()
40+
41+
export function DataListDescriptorDescendantContextProvider({
42+
children,
43+
}: DataListDescriptorDescendantContextProviderProps) {
44+
return <DescendantsContextProvider>{children}</DescendantsContextProvider>
45+
}
46+
47+
export const useDataListDescriptorDescendant = useDescendant as typeof useDescendant

src/data-list-descriptors.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import * as React from "react"
2+
3+
import type {
4+
DataListDescriptor,
5+
DataListDescriptors as DataListDescriptorsType,
6+
} from "./data-list-types"
7+
import {
8+
DataListDescriptorContextProvider,
9+
type DataListDescriptorContextProviderContext,
10+
DataListDescriptorDescendantContextProvider,
11+
} from "./data-list-descriptor-context"
12+
13+
export interface DataListDescriptorsProps<TRenderItem>
14+
extends Omit<
15+
DataListDescriptorContextProviderContext,
16+
"children" | "attachDescriptors" | "updateDescriptorIndex"
17+
> {
18+
descriptors: React.ReactNode
19+
setDescriptors: React.Dispatch<React.SetStateAction<DataListDescriptorsType<TRenderItem>>>
20+
}
21+
22+
export function DataListDescriptors<TRenderItem>({
23+
descriptors,
24+
setDescriptors,
25+
}: DataListDescriptorsProps<TRenderItem>) {
26+
const attachDescriptors: DataListDescriptorContextProviderContext["attachDescriptors"] =
27+
React.useCallback(
28+
(index: number, additionalDescriptors: Array<DataListDescriptor<TRenderItem>>) => {
29+
setDescriptors((prevDescriptors) => {
30+
const newDescriptors = new Map(prevDescriptors)
31+
newDescriptors.set(index, additionalDescriptors)
32+
return newDescriptors
33+
})
34+
35+
return () => {
36+
setDescriptors((prevDescriptors) => {
37+
const newDescriptors = new Map(prevDescriptors)
38+
newDescriptors.delete(index)
39+
return newDescriptors
40+
})
41+
}
42+
},
43+
[setDescriptors]
44+
)
45+
46+
return (
47+
<DataListDescriptorContextProvider attachDescriptors={attachDescriptors}>
48+
<DataListDescriptorDescendantContextProvider>
49+
{descriptors}
50+
</DataListDescriptorDescendantContextProvider>
51+
</DataListDescriptorContextProvider>
52+
)
53+
}

src/data-list-master-renderer.tsx

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import React, {useDeferredValue} from "react"
2+
3+
import type {
4+
DataListDescriptors,
5+
DataListRenderer,
6+
DataListRendererProps as DataListRendererPropsType,
7+
DataListRendererRootRenderItem,
8+
DataListRendererRootRenderItemArgs,
9+
DataListRenderListItemInfo,
10+
} from "./data-list-types"
11+
import {DataListRendererContextProvider} from "./data-list-renderer-context"
12+
import {cancelFrame, requestFrame} from "./utils/request-frame"
13+
14+
export interface DataListMasterRendererProps<TRenderItem>
15+
extends Omit<DataListRendererPropsType<TRenderItem>, "data" | "rootRenderItem" | "getItemId"> {
16+
renderer: DataListRenderer<TRenderItem> | ReturnType<DataListRenderer<TRenderItem>>
17+
descriptors: DataListDescriptors<TRenderItem>
18+
}
19+
20+
export function DataListMasterRenderer<TRenderItem>({
21+
renderer,
22+
descriptors,
23+
...rest
24+
}: DataListMasterRendererProps<TRenderItem>) {
25+
const newData = React.useMemo((): Array<
26+
DataListRendererRootRenderItemArgs<TRenderItem>["item"]
27+
> => {
28+
const sortedDescriptors = [...descriptors.entries()].sort((a, b) => a[0] - b[0])
29+
30+
return sortedDescriptors.flatMap(([index, descriptors]) =>
31+
descriptors.map((d) => ({
32+
descriptor: {
33+
...d,
34+
id: [index, ...(Array.isArray(d.id) ? d.id : [d.id])],
35+
},
36+
descriptorSourceIndex: index,
37+
}))
38+
)
39+
}, [descriptors])
40+
41+
const deferredData = useDeferredValue(newData)
42+
43+
const hasInitialPaintedRef = React.useRef(false)
44+
React.useLayoutEffect(() => {
45+
if (!hasInitialPaintedRef.current) {
46+
const id = requestFrame(() => {
47+
hasInitialPaintedRef.current = true
48+
})
49+
50+
return () => cancelFrame(id)
51+
}
52+
53+
return
54+
}, [])
55+
56+
// Only use deferred data after first mount/paint.
57+
const data = hasInitialPaintedRef.current ? deferredData : newData
58+
59+
const rootRenderItem: DataListRendererRootRenderItem<TRenderItem> = React.useCallback(
60+
({item, index}) => item.descriptor.render({...item, index, allData: data}),
61+
[data]
62+
)
63+
64+
const getItemId = React.useCallback(
65+
(item: DataListRenderListItemInfo<TRenderItem>) =>
66+
(Array.isArray(item.descriptor.id) ? item.descriptor.id : [item.descriptor.id]).join(
67+
":"
68+
),
69+
[]
70+
)
71+
72+
const extraProps = {data, rootRenderItem, getItemId}
73+
74+
return (
75+
<DataListRendererContextProvider {...rest} {...extraProps}>
76+
{typeof renderer === "function" ? renderer({...rest, ...extraProps}) : renderer}
77+
</DataListRendererContextProvider>
78+
)
79+
}

0 commit comments

Comments
 (0)