FlashList

repository·main·Indexed 27 days ago

https://github.com/shopify/flash-list

A high-performance replacement for React Native's FlatList that uses view recycling to provide smooth scrolling, reduce memory usage, and eliminate blank cells. Version 2.x is designed exclusively for React Native's New Architecture. It includes utilities like LayoutCommitObserver for tracking layout completion, useRecyclingState for managing state during component recycling, and useMappingHelper for stable key strategies.

Tokens
16.1K
Snippets
45
Records
96
Agent score
91%

What's inside @shopify/flash-list

  1. Avoid using the `key` prop in item components

    main

    Do not use the key prop inside your item components or their nested components. Using a key that changes between data items forces React to treat the component as entirely new, preventing FlashList from recycling the view and destroying the performance benefits.

    Incorrect:

    const MyItem = ({ item }) => {
      return <View key={item.id}><Text>{item.title}</Text></View>;
    };

    Correct:

    const MyItem = ({ item }) => {
      return <View><Text>{item.title}</Text></View>;
    };
  2. Migrate MasonryFlashList to FlashList with masonry prop

    main

    The MasonryFlashList component is deprecated in v2. Instead, import FlashList and use the masonry prop. Note that getColumnFlex is not supported in v2.

    // v2
    import { FlashList } from "@shopify/flash-list";
    
    <FlashList
      data={data}
      renderItem={renderItem}
      numColumns={3}
      masonry
    />
  3. Enable Masonry Layout in FlashList (v2)

    main

    To create a grid of items with different heights (e.g., an image gallery), use the masonry prop on the FlashList component. You must also specify the number of columns using numColumns.

    import React from "react";
    import { Text } from "react-native";
    import { FlashList } from "@shopify/flash-list";
    import { DATA } from "./data";
    
    const MyMasonryList = () => {
      return (
        <FlashList
          data={DATA}
          masonry
          numColumns={2}
          renderItem={({ item }) => <Text>{item.title}</Text>}
        />
      );
    };
  4. Use LayoutAnimation with FlashList

    main

    To use React Native's LayoutAnimation with FlashList, you must call the prepareForLayoutAnimationRender() instance method on your FlashList reference before calling LayoutAnimation.configureNext(). Additionally, you must provide a keyExtractor prop to your FlashList component to ensure elements are uniquely identified for the animation.

    Note: LayoutAnimation is experimental on Android, and stability cannot be guaranteed when used with FlashList on that platform.

    import React, { useRef, useState } from "react";
    import { View, Text, Pressable, LayoutAnimation } from "react-native";
    import { FlashList } from "@shopify/flash-list";
    
    const List = () => {
      const [data, setData] = useState([1, 2, 3, 4, 5]);
    
      const list = useRef<FlashList<number> | null>(null);
    
      const removeItem = (item: number) => {
        setData(
          data.filter((dataItem) => {
            return dataItem !== item;
          })
        );
        // This must be called before `LayoutAnimation.configureNext` in order for the animation to run properly.
        list.current?.prepareForLayoutAnimationRender();
        // After removing the item, we can start the animation.
        LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
      };
    
      const renderItem = ({ item }: { item: number }) => {
        return (
          <Pressable
            onPress={() => {
              removeItem(item);
            }}
          >
            <View>
              <Text>Cell Id: {item}</Text>
            </View>
          </Pressable>
        );
      };
    
      return (
        <FlashList
          // Saving reference to the `FlashList` instance to later trigger `prepareForLayoutAnimationRender` method.
          ref={list}
          // This prop is necessary to uniquely identify the elements in the list.
          keyExtractor={(item: number) => {
            return item.toString();
          }}
          renderItem={renderItem}
          data={data}
        />
      );
    };
    
    export default List;
  5. Use LayoutCommitObserver to track FlashList layout completion

    main

    The LayoutCommitObserver is a utility component used to track when all FlashList components within its component tree have completed their layout. This is useful for coordinating complex UI behaviors that depend on list rendering completion, such as measuring view sizes after all internal lists have rendered, especially when you do not have direct access to the FlashList instances (e.g., when your component only accepts a children prop).

    Important Considerations:

    • The onCommitLayoutEffect callback fires after every layout operation, not just the initial one.
    • Performance Warning: Performing a setState inside the callback will block paint until the state change is ready to be committed.
    • Alternatives:
      • If you do not need to block paint, use the onLoad callback instead.
      • If you only have a single FlashList and have direct access to it, use the onCommitLayoutEffect prop available directly on the FlashList component.
    import { LayoutCommitObserver } from "@shopify/flash-list";
    
    function MyScreen() {
      const handleLayoutComplete = () => {
        console.log("All FlashLists have completed their initial layout!");
        // Perform any post-layout actions here
      };
    
      return (
        <LayoutCommitObserver onCommitLayoutEffect={handleLayoutComplete}>
          <View>
            <FlashList data={data1} renderItem={renderItem1} />
            <FlashList data={data2} renderItem={renderItem2} />
          </View>
        </LayoutCommitObserver>
      );
    }
  6. Setup FlashList for Jest testing

    main

    By default, FlashList mounts all items in a test environment, which can lead to performance issues or incorrect test behavior. To prevent this, use the provided Jest setup file to mock measurements.

    1. Add require("@shopify/flash-list/jestSetup"); to your jest-setup.js file.
    2. Ensure your jest.config.js is configured to use your setup file and the react-native preset.
    // In jest-setup.js
    require("@shopify/flash-list/jestSetup");
    
    // In jest.config.js
    ...
    preset: 'react-native',
    setupFiles: ['./jest-setup.js'],
    ...
  7. Optimize item components for recycling

    main

    When an item moves out of the viewport, FlashList recycles the component by re-rendering it with a different item prop instead of destroying it. To ensure high performance:

    1. Minimize re-renders: Ensure as few things as possible are re-computed during recycling.
    2. Memoize props: In v2, it is critical to memoize props passed to FlashList to prevent unnecessary updates.
    3. Use memo for leaf components: If a component within your item does not depend on the item prop, wrap it in memo to skip re-renders during recycling.
    const MyHeavyComponent = () => {
      return ...;
    };
    
    const MemoizedMyHeavyComponent = memo(MyHeavyComponent);
    
    const MyItem = ({ item }: { item: any }) => {
      return (
        <>
          <MemoizedMyHeavyComponent />
          <Text>{item.title}</Text>
        </>
      );
    };
  8. Setup and run the Universal React Project

    main

    To set up and run this project, follow these steps:

    1. Install dependencies: Use yarn or npm install.
    2. Install native pods (iOS only): If you have native iOS code, run npx pod-install.
    3. Start the bundler: Run yarn start or npm run start.
    4. Open the project:
    yarn install
    npx pod-install
    yarn start
  9. Migrate from SectionList to FlashList

    main

    Since FlashList does not support SectionList specific props like sections, renderSectionHeader, or renderSectionFooter, you must flatten your data into a single array and use type checking within renderItem to distinguish between section headers and list items.

    Migration Steps:

    1. Flatten Data: Convert your nested section objects into a single array containing both the header values (e.g., strings) and the data items.
    2. Update renderItem: Use typeof or a similar type guard to determine if the current item is a header or a data row.
    3. Implement getItemType: For optimal performance, provide a getItemType function that returns a unique identifier for each type of item (e.g., 'sectionHeader' vs 'row'). This helps FlashList recycle components more efficiently.
    4. Handle Sticky Headers: To replicate stickySectionHeadersEnabled, pre-calculate an array of indices for stickyHeaderIndices that points to the positions of your header items in the flattened array.
    import React from "react";
    import { StyleSheet, Text } from "react-native";
    import { FlashList } from "@shopify/flash-list";
    
    interface Contact {
      firstName: string;
      lastName: string;
    }
    
    // 1. Flattened data containing both headers (strings) and items (Contact)
    const contacts: (string | Contact)[] = [
      "A",
      { firstName: "John", lastName: "Aaron" },
      "D",
      { firstName: "John", lastName: "Doe" },
      { firstName: "Mary", lastName: "Dianne" },
    ];
    
    // 2. Pre-calculate indices for sticky headers
    const stickyHeaderIndices = contacts
      .map((item, index) => {
        if (typeof item === "string") {
          return index;
        } else {
          return null;
        }
      })
      .filter((item) => item !== null) as number[];
    
    const ContactsFlashList = () => {
      return (
        <FlashList
          data={contacts}
          renderItem={({ item }) => {
            if (typeof item === "string") {
              // Rendering header
              return <Text style={styles.header}>{item}</Text>;
            } else {
              // Render item
              return <Text>{item.firstName}</Text>;
            }
          }}
          stickyHeaderIndices={stickyHeaderIndices}
          getItemType={(item) => {
            // 3. Specify type for better performance
            return typeof item === "string" ? "sectionHeader" : "row";
          }}
        />
      );
    };
    
    const styles = StyleSheet.create({
      header: {
        fontSize: 32,
        backgroundColor: "#fff",
      },
    });
  10. Generate and modify native iOS and Android projects

    main
    You can generate native iOS and Android projects from your Expo configuration file (app.json or app.config.js) by running npx expo prebuild. Once generated, these native projects can be compiled and managed using Xcode and Android Studio.
    npx expo prebuild