|
1 | 1 | # React Data List |
2 | 2 |
|
3 | | -A library for composing data arrays using React components. |
| 3 | +React Data List is a library which helps you build data arrays by expressing items declaratively using React components. It was built primarily for React Native, but should work anywhere. |
4 | 4 |
|
5 | 5 | ## Why? |
6 | 6 |
|
7 | | -Universal List is an API for composing together lists. |
8 | | - |
9 | | -In React Native world, virtualized lists are modelled with a flat array of data, along with a render function that describes how to render items. Most of our screens tend to be represented virtualized lists. It's a pillar to how we build. |
10 | | - |
11 | | -However, building this data array can be really difficult. The straightforward approach is to simply have a dataFetchable that's the result of a single useNexusSelector call. However quickly becomes a problem when you want to have granular control over loading states (e.g. if we fail to load a single notification then don't poison the entire fetchable) or especially when you want to compose familiar data & render logic across screens (e.g. task picker items on the record detail page). When you need to solve both of those problems you quickly end up in a real mess with data selectors that are hundreds of lines long and not approachable. |
12 | | - |
13 | | -Universal List allows you to model the creation of this data array in React JSX, encouraging the use of Suspense and Error Boundaries to achieve the behaviors you want. Nexus has first-class support for these wider React concepts, so the ergonomics are quite nice. |
| 7 | +For a full explanation around why this exists, please checkout the counterpart [article here](https://attio.com). |
| 8 | + |
| 9 | +## |
| 10 | + |
| 11 | +## How does it look? |
| 12 | + |
| 13 | +### Example 1 - Basic |
| 14 | + |
| 15 | +```tsx |
| 16 | +function ErrorOnRenderCheck() { |
| 17 | + return ( |
| 18 | + <View |
| 19 | + onLayout={() => { |
| 20 | + throw new Error("This never happens as we evaluate descriptors before first paint.") |
| 21 | + }} |
| 22 | + /> |
| 23 | + ) |
| 24 | +} |
| 25 | + |
| 26 | +<ReactDataList |
| 27 | + renderer={ |
| 28 | + <FlashListRenderer |
| 29 | + contentContainerStyle={contentContainerStyle} |
| 30 | + ListHeaderComponent={ |
| 31 | + <Header |
| 32 | + toggleThorinAndCompany={toggleThorinAndCompany} |
| 33 | + jumbleFellowship={jumbleFellowship} |
| 34 | + /> |
| 35 | + } |
| 36 | + /> |
| 37 | + } |
| 38 | + renderEmpty={() => ( |
| 39 | + /** |
| 40 | + * This never happens as we're guaranteed rows will be |
| 41 | + * evaluated to data items on first paint. |
| 42 | + */ |
| 43 | + <View style={styles.alert}> |
| 44 | + <ErrorOnRenderCheck /> |
| 45 | + </View> |
| 46 | + )} |
| 47 | +> |
| 48 | + {/* Supports nested ReactDataList instances */} |
| 49 | + <ListHeaderDataListRow title="Places in Middle Earth" /> |
| 50 | + <MiddleEarthPlacesDataListRow placeItems={MIDDLE_EARTH_PLACES} /> |
| 51 | + |
| 52 | + <ListHeaderDataListRow title="The Fellowship" /> |
| 53 | + {fellowship.map((character) => ( |
| 54 | + <CharacterListItemDataListRow |
| 55 | + key={character.name} |
| 56 | + name={character.name} |
| 57 | + race={character.race} |
| 58 | + url={character.url} |
| 59 | + /> |
| 60 | + ))} |
| 61 | + |
| 62 | + {isShowingThorinAndCompany && ( |
| 63 | + <> |
| 64 | + <ListHeaderDataListRow title="Thorin and Company" /> |
| 65 | + <React.Suspense fallback={<LoadingListItemDataListRow />}> |
| 66 | + <MiddleEarthHobbitCompanyDataListRows /> |
| 67 | + </React.Suspense> |
| 68 | + </> |
| 69 | + )} |
| 70 | +</ReactDataList> |
| 71 | +``` |
14 | 72 |
|
15 | | -## How does it work? |
| 73 | +https://github.com/user-attachments/assets/23c2d232-7f35-470e-98b4-156efaaf326a |
| 74 | + |
| 75 | +### Example 2 - Fetchable Template |
| 76 | + |
| 77 | +A lightweight wrapper around `ReactDataList` which provides a top-level [React.Suspense](https://react.dev/reference/react/Suspense) and [ErrorBoundary](https://react.dev/reference/react/Component#static-getderivedstatefromerror). This is useful for typical async work, where you may want to display skeleton rows (via `renderPending`), a full-screen spinner (via `renderPending`), or perhaps a full-screen error message (via `renderError`). |
| 78 | + |
| 79 | +```tsx |
| 80 | +<ReactDataList.Fetchable |
| 81 | + renderer={ |
| 82 | + <FlashListRenderer |
| 83 | + contentContainerStyle={contentContainerStyle} |
| 84 | + ListHeaderComponent={<Header reload={reload} />} |
| 85 | + /> |
| 86 | + } |
| 87 | + renderPendingRows={ |
| 88 | + <> |
| 89 | + <ListHeaderDataListRow title="Thorin and Company" /> |
| 90 | + <LoadingListItemDataListRow /> |
| 91 | + <LoadingListItemDataListRow /> |
| 92 | + <LoadingListItemDataListRow /> |
| 93 | + <LoadingListItemDataListRow /> |
| 94 | + <LoadingListItemDataListRow /> |
| 95 | + <LoadingListItemDataListRow /> |
| 96 | + </> |
| 97 | + } |
| 98 | +> |
| 99 | + <ListHeaderDataListRow title="Thorin and Company" /> |
| 100 | + <MiddleEarthHobbitCompanyDataListRows key={reloadKey} /> |
| 101 | +</ReactDataList.Fetchable> |
| 102 | +``` |
16 | 103 |
|
17 | | -There are two main concepts: **row** and **renderer**. |
18 | | -A **row** syncs some data and a render function to the list. The universal list then processes this into an aggregated data array, render function and other typical list functions. These then get passed into a renderer which takes the Universal List format and translates it into inputs for some list component, like FlashList. We refer to the item data that a row syncs as its **descriptor** |
| 104 | +https://github.com/user-attachments/assets/8ab7b63e-1ddc-40f9-8d20-6d443f21c934 |
19 | 105 |
|
20 | 106 | ## Release Process |
21 | 107 |
|
|
0 commit comments