From 1ac287046619d900d3c3d3b79b141ca2cff761e7 Mon Sep 17 00:00:00 2001 From: Mark Rickert <139261+markrickert@users.noreply.github.com> Date: Wed, 11 Oct 2023 12:32:21 -0600 Subject: [PATCH] feat(boilerplate) `@shopify/flash-list` integration and ListView HOC (#2513) * feat(boilerplate) @shopify/flash-list integration This integrates @shopify/flash-list in all places where we used to have a FlatList and updates the expo to compatibility object to use the correct expected version while using the latest for expo prebuild and bare workflows. * fix: ios compiling issue when the user specifies the new architecture * fix: Get FlashList working in new architecture using interop layer iOS seems to have problems with flashlist using interop, but android seems to work just fine. * feat: adds ListView Higher Order Component Displays a FlashList or FlatList depending on language RTL features. Needs a little more work on the `ref` passing. * chore: formatting * refactor(boilerplate): ListView HOC types * chore(tests) Tests passing after adding new ListView component * ci: bun version lock --------- Co-authored-by: Frank Calise --- .circleci/config.yml | 3 +- README.md | 1 + boilerplate/app/components/ListView.tsx | 44 +++++++++++++++++++ boilerplate/app/components/index.ts | 1 + .../app/screens/DemoPodcastListScreen.tsx | 16 +++---- .../DemoShowroomScreen/DemoShowroomScreen.tsx | 15 ++++--- .../DemoShowroomScreen/demos/DemoListItem.tsx | 19 ++++---- boilerplate/package.json | 1 + boilerplate/react-native.config.js | 14 ++++++ docs/Components-ListView.md | 13 ++++++ docs/Folder-Structure.md | 1 + docs/README.md | 1 + src/commands/new.ts | 1 - src/tools/expoGoCompatibility.ts | 1 + test/vanilla/ignite-generate.test.ts | 2 + 15 files changed, 104 insertions(+), 29 deletions(-) create mode 100644 boilerplate/app/components/ListView.tsx create mode 100644 docs/Components-ListView.md diff --git a/.circleci/config.yml b/.circleci/config.yml index 77e6abb37..c57fa888b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,7 +19,8 @@ jobs: resource_class: large steps: - checkout - - bun-orb/setup + - bun-orb/setup: + version: 1.0.3 - restore_cache: name: Restore node modules keys: diff --git a/README.md b/README.md index 6cfeb6173..e9db00997 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ Nothing makes it into Ignite unless it's been proven on projects that Infinite R | Jest | Test Runner | v26 | Standard test runner for JS apps | | Maestro | Testing Framework | | Automate end-to-end UI testing | | date-fns | Date library | v2 | Excellent date library | +| FlashList | FlatList replacement | v1 | A performant drop-in replacement for FlatList | Ignite also comes with a [component library](https://github.com/infinitered/ignite/blob/master/docs/Components.md) that is tuned for custom designs, theming support, testing, custom fonts, generators, and much, much more. diff --git a/boilerplate/app/components/ListView.tsx b/boilerplate/app/components/ListView.tsx new file mode 100644 index 000000000..3b12bc212 --- /dev/null +++ b/boilerplate/app/components/ListView.tsx @@ -0,0 +1,44 @@ +import React, { forwardRef, PropsWithoutRef } from "react" +import { FlatList } from "react-native" +import { isRTL } from "app/i18n" +import { FlashList, FlashListProps } from "@shopify/flash-list" + +export type ListViewRef = FlashList | FlatList + +export type ListViewProps = PropsWithoutRef> + +/** + * This is a Higher Order Component meant to ease the pain of using @shopify/flash-list + * when there is a chance that a user would have their device language set to an + * RTL language like Arabic or Punjabi. This component will use react-native's + * FlatList if the user's language is RTL or FlashList if the user's language is LTR. + * + * Because FlashList's props are a superset of FlatList's, you must pass estimatedItemSize + * to this component if you want to use it. + * + * This is a temporary workaround until the FlashList component supports RTL at + * which point this component can be removed and we will default to using FlashList everywhere. + * + * @see {@link https://github.com/Shopify/flash-list/issues/544|RTL Bug Android} + * @see {@link https://github.com/Shopify/flash-list/issues/840|Flashlist Not Support RTL} + * + * @param props - FlashListProps | FlatListProps + * @param forwardRef - React.Ref> + * @returns JSX.Element + */ + +const ListViewComponent = forwardRef( + (props: ListViewProps, ref: React.ForwardedRef>) => { + const ListComponentWrapper = isRTL ? FlatList : FlashList + + return + }, +) + +ListViewComponent.displayName = "ListView" + +export const ListView = ListViewComponent as ( + props: ListViewProps & { + ref?: React.RefObject> + }, +) => React.ReactElement diff --git a/boilerplate/app/components/index.ts b/boilerplate/app/components/index.ts index c65532ff4..43506380f 100644 --- a/boilerplate/app/components/index.ts +++ b/boilerplate/app/components/index.ts @@ -4,6 +4,7 @@ export * from "./Card" export * from "./Header" export * from "./Icon" export * from "./ListItem" +export * from "./ListView" export * from "./Screen" export * from "./Text" export * from "./TextField" diff --git a/boilerplate/app/screens/DemoPodcastListScreen.tsx b/boilerplate/app/screens/DemoPodcastListScreen.tsx index e9a4c5c90..b027c3c8c 100644 --- a/boilerplate/app/screens/DemoPodcastListScreen.tsx +++ b/boilerplate/app/screens/DemoPodcastListScreen.tsx @@ -1,11 +1,8 @@ -// Interested in migrating from FlatList to FlashList? Check out the recipe in our Ignite Cookbook -// https://ignitecookbook.com/docs/recipes/MigratingToFlashList import { observer } from "mobx-react-lite" import React, { FC, useEffect, useMemo } from "react" import { AccessibilityProps, ActivityIndicator, - FlatList, Image, ImageStyle, Platform, @@ -14,6 +11,7 @@ import { View, ViewStyle, } from "react-native" +import { type ContentStyle } from "@shopify/flash-list" import Animated, { Extrapolate, interpolate, @@ -21,7 +19,7 @@ import Animated, { useSharedValue, withSpring, } from "react-native-reanimated" -import { Button, Card, EmptyState, Icon, Screen, Text, Toggle } from "../components" +import { Button, Card, EmptyState, Icon, ListView, Screen, Text, Toggle } from "../components" import { isRTL, translate } from "../i18n" import { useStores } from "../models" import { Episode } from "../models/Episode" @@ -66,11 +64,12 @@ export const DemoPodcastListScreen: FC> = safeAreaEdges={["top"]} contentContainerStyle={$screenContentContainer} > - - data={episodeStore.episodesForList} + + contentContainerStyle={$listContentContainer} + data={episodeStore.episodesForList.slice()} extraData={episodeStore.favorites.length + episodeStore.episodes.length} - contentContainerStyle={$flatListContentContainer} refreshing={refreshing} + estimatedItemSize={177} onRefresh={manualRefresh} ListEmptyComponent={ isLoading ? ( @@ -118,7 +117,6 @@ export const DemoPodcastListScreen: FC> = } renderItem={({ item }) => ( episodeStore.toggleFavorite(item)} @@ -296,7 +294,7 @@ const $screenContentContainer: ViewStyle = { flex: 1, } -const $flatListContentContainer: ViewStyle = { +const $listContentContainer: ContentStyle = { paddingHorizontal: spacing.lg, paddingTop: spacing.lg + spacing.xl, paddingBottom: spacing.lg, diff --git a/boilerplate/app/screens/DemoShowroomScreen/DemoShowroomScreen.tsx b/boilerplate/app/screens/DemoShowroomScreen/DemoShowroomScreen.tsx index b0f133c63..2471db44a 100644 --- a/boilerplate/app/screens/DemoShowroomScreen/DemoShowroomScreen.tsx +++ b/boilerplate/app/screens/DemoShowroomScreen/DemoShowroomScreen.tsx @@ -2,7 +2,6 @@ import { Link, RouteProp, useRoute } from "@react-navigation/native" import React, { FC, ReactElement, useEffect, useRef, useState } from "react" import { Dimensions, - FlatList, Image, ImageStyle, Platform, @@ -11,9 +10,10 @@ import { View, ViewStyle, } from "react-native" +import { type ContentStyle } from "@shopify/flash-list" import { DrawerLayout, DrawerState } from "react-native-gesture-handler" import { useSharedValue, withTiming } from "react-native-reanimated" -import { ListItem, Screen, Text } from "../../components" +import { ListItem, ListView, ListViewRef, Screen, Text } from "../../components" import { isRTL } from "../../i18n" import { DemoTabParamList, DemoTabScreenProps } from "../../navigators/DemoNavigator" import { colors, spacing } from "../../theme" @@ -88,7 +88,7 @@ export const DemoShowroomScreen: FC> = const timeout = useRef>() const drawerRef = useRef(null) const listRef = useRef(null) - const menuRef = useRef(null) + const menuRef = useRef>(null) const progress = useSharedValue(0) const route = useRoute>() const params = route.params @@ -181,12 +181,13 @@ export const DemoShowroomScreen: FC> = - + ref={menuRef} - contentContainerStyle={$flatListContentContainer} + contentContainerStyle={$listContentContainer} + estimatedItemSize={250} data={Object.values(Demos).map((d) => ({ name: d.name, - useCases: d.data.map((u) => u.props.name), + useCases: d.data.map((u) => u.props.name as string), }))} keyExtractor={(item) => item.name} renderItem={({ item, index: sectionIndex }) => ( @@ -237,7 +238,7 @@ const $drawer: ViewStyle = { flex: 1, } -const $flatListContentContainer: ViewStyle = { +const $listContentContainer: ContentStyle = { paddingHorizontal: spacing.lg, } diff --git a/boilerplate/app/screens/DemoShowroomScreen/demos/DemoListItem.tsx b/boilerplate/app/screens/DemoShowroomScreen/demos/DemoListItem.tsx index 615b29aba..fb8ccd3e5 100644 --- a/boilerplate/app/screens/DemoShowroomScreen/demos/DemoListItem.tsx +++ b/boilerplate/app/screens/DemoShowroomScreen/demos/DemoListItem.tsx @@ -1,14 +1,13 @@ /* eslint-disable react/jsx-key, react-native/no-inline-styles */ import React from "react" import { TextStyle, View, ViewStyle } from "react-native" -import { FlatList } from "react-native-gesture-handler" -import { Icon, ListItem, Text } from "../../../components" +import { Icon, ListItem, ListView, Text } from "../../../components" import { colors, spacing } from "../../../theme" import { Demo } from "../DemoShowroomScreen" import { DemoDivider } from "../DemoDivider" import { DemoUseCase } from "../DemoUseCase" -const flatListData = +const listData = `Tempor Id Ea Aliqua Pariatur Aliquip. Irure Minim Voluptate Consectetur Consequat Sint Esse Proident Irure. Nostrud Elit Veniam Nostrud Excepteur Minim Deserunt Quis Dolore Velit Nulla Irure Voluptate Tempor. Occaecat Amet Laboris Nostrud Qui Do Quis Lorem Ex Elit Fugiat Deserunt. In Pariatur Excepteur Exercitation Ex Incididunt Qui Mollit Dolor Sit Non. Culpa Officia Minim Cillum Exercitation Voluptate Proident Laboris Et Est Reprehenderit Quis Pariatur Nisi` .split(".") .map((item) => item.trim()) @@ -36,11 +35,10 @@ const $customContainerStyle: ViewStyle = { borderTopColor: colors.palette.neutral100, } -const $flatListStyle: ViewStyle = { +const $listStyle: ViewStyle = { + height: 148, paddingHorizontal: spacing.xs, backgroundColor: colors.palette.neutral200, - flex: 1, - overflow: "scroll", } export const DemoListItem: Demo = { @@ -145,13 +143,12 @@ export const DemoListItem: Demo = { , - - - data={flatListData} - style={$flatListStyle} + + + data={listData} renderItem={({ item, index }) => ( [Please see the FlashList documentation for more information on the props that are available.](https://shopify.github.io/flash-list/docs/usage) + +### `estimatedItemSize` + +> [Please see the FlashList documentation for more information on this prop.](https://shopify.github.io/flash-list/docs/estimated-item-size) diff --git a/docs/Folder-Structure.md b/docs/Folder-Structure.md index ba9dcea60..e6ca2c19d 100644 --- a/docs/Folder-Structure.md +++ b/docs/Folder-Structure.md @@ -65,6 +65,7 @@ This is where your components will live, the reusable building blocks to create - [Header](./Components-Header.md) - [Icon](./Components-Icon.md) - [ListItem](./Components-ListItem.md) +- [ListView](./Components-ListView.md) - [Screen](./Components-Screen.md) - [Text](./Components-Text.md) - [TextField](./Components-TextField.md) diff --git a/docs/README.md b/docs/README.md index c4e1b6008..9de46a48e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,6 +11,7 @@ Check out this list of topics: - [Header](./Components-Header.md) - [Icon](./Components-Icon.md) - [ListItem](./Components-ListItem.md) + - [ListView](./Components-ListView.md) - [Screen](./Components-Screen.md) - [Text](./Components-Text.md) - [TextField](./Components-TextField.md) diff --git a/src/commands/new.ts b/src/commands/new.ts index bb2497629..37d2ea8c6 100644 --- a/src/commands/new.ts +++ b/src/commands/new.ts @@ -621,7 +621,6 @@ module.exports = { startSpinner(" Enabling New Architecture") try { const appJsonRaw = read("app.json") - const appJson = JSON.parse(appJsonRaw) appJson.expo.plugins[1][1].ios.newArchEnabled = true appJson.expo.plugins[1][1].android.newArchEnabled = true diff --git a/src/tools/expoGoCompatibility.ts b/src/tools/expoGoCompatibility.ts index 66f3f604a..9e2a42471 100644 --- a/src/tools/expoGoCompatibility.ts +++ b/src/tools/expoGoCompatibility.ts @@ -9,6 +9,7 @@ export const expoGoCompatExpectedVersions = { "expo-file-system": "~15.4.4", "expo-font": "~11.4.0", "expo-localization": "~14.3.0", + "@shopify/flash-list": "1.4.3", } // This function takes a package.json file as a string and updates the versions of the diff --git a/test/vanilla/ignite-generate.test.ts b/test/vanilla/ignite-generate.test.ts index 7353541ac..4d0198fdd 100644 --- a/test/vanilla/ignite-generate.test.ts +++ b/test/vanilla/ignite-generate.test.ts @@ -276,6 +276,7 @@ describe("ignite-cli generate", () => { export * from \\"./Header\\" export * from \\"./Icon\\" export * from \\"./ListItem\\" + export * from \\"./ListView\\" export * from \\"./Screen\\" export * from \\"./Text\\" export * from \\"./TextField\\" @@ -342,6 +343,7 @@ describe("ignite-cli generate", () => { export * from \\"./Header\\" export * from \\"./Icon\\" export * from \\"./ListItem\\" + export * from \\"./ListView\\" export * from \\"./Screen\\" export * from \\"./Text\\" export * from \\"./TextField\\"