React Native Skia List

repository·main·Indexed 42 days ago

https://github.com/samuelscheit/react-native-skia-list

A high-performance list renderer for React Native built on @shopify/react-native-skia. It provides SkiaFlatList for rendering large datasets with smooth 2D graphics and SkiaScrollView as a Skia-powered replacement for the standard ScrollView. The library utilizes worklets for performance and includes the useSkiaFlatList and useSkiaScrollView hooks for programmatic state management, including methods for scrolling, data manipulation, and touch detection via getItemFromTouch.

Tokens
11.8K
Snippets
22
Records
41
Agent score
53%

What's inside React Native Skia List

  1. Manage SkiaScrollView state with useSkiaScrollView

    main

    For custom behaviors (like custom gestures or renderers) or for high-performance updates, use the useSkiaScrollView hook instead of passing children.

    Using the hook allows you to imperatively update the list content via state.content.value, which avoids the React reconciler and improves performance for frequent updates.

    Example: High-frequency updates

    const state = useSkiaScrollView({ height: 1000 });
    const content = state.content.value;
    
    let previous = null;
    // Add a new rect every 10ms without triggering React re-renders
    setInterval(() => {
    	if (previous) content.removeChild(previous);
    
    	previous = SkiaDomApi.RectNode({
    		width: 100, height: 100,
    		x: Math.random() * 1000,
    		y: Math.random() * 1000
    	});
    
    	content.addChild(previous);
    }, 10);
    
    <SkiaScrollView list={state} style={{ flex: 1 }} />
    const state = useSkiaScrollView({ height: 1000 });
    const content = state.content.value;
    
    content.addChild(SkiaDomApi.RectNode({ width: 100, height: 100, x: 0, y: 0 }));
    
    <SkiaScrollView list={state} style={{ flex: 1 }} />
  2. Understand the React Native Skia List mental model

    main

    React Native Skia List is a high-performance list component built on top of the Skia 2D graphics library.

    Key distinction: Unlike standard React Native list libraries, this component does not have a layout engine. As a user, you are responsible for calculating the layout of your list items. While this increases implementation complexity, it enables significantly higher performance and greater customization compared to libraries like FlashList.

  3. Install react-native-skia-list

    main

    Install react-native-skia-list along with its required peer dependencies using yarn. Note that this library is currently in early development and is not recommended for production use.

    yarn add react-native-skia-list @shopify/react-native-skia react-native-keyboard-controller react-native-reanimated react-native-gesture-handler
  4. Use SkiaScrollView for Skia-based scrolling

    main

    <SkiaScrollView /> is a replacement for the React Native <ScrollView />. It uses the Skia rendering engine, which means you cannot use standard React Native components inside it; you must use Skia elements.

    Critical Requirement: You must specify the height prop (from ScrollGestureProps) to make the list scrollable. Without this, the content will not scroll.

    To ensure the canvas fills the screen, it is recommended to set style={{ flex: 1 }}.

    const paint = Skia.Paint();
    paint.setColor(Skia.Color("rgb(91, 128, 218)"));
    
    const circleCount = 100;
    
    <SkiaScrollView height={circleCount * 100} style={{ flex: 1 }}>
    	{Array.from({ length: circleCount }, (_, i) => (
    		<Circle key={i} cx={50} cy={50 + i * 100} r={50} paint={paint} />
    	))}
    </SkiaScrollView>
  5. Use SkiaFlatList to render high-performance lists

    main

    Use <SkiaFlatList /> as a high-performance replacement for React Native's <FlatList />.

    Important Constraints:

    • It uses the Skia Rendering Engine, meaning you cannot use standard React Native components inside it.
    • It inherits all props from <SkiaScrollView />.
    • You must manually calculate or provide the layout and dimensions of list items (e.g., using Skia's measureText() or Paragraph getHeight() API).

    To ensure smooth performance, use initialData (a function returning an array that can be serialized to the worklet thread) and transformItem to pre-process data on the worklet thread.

    import { SkiaFlatList } from "react-native-skia-list";
    
    // ... setup paragraphBuilder ...
    
    <SkiaFlatList
      safeArea={safeArea}
      initialData={() => [0, 1, 2, 3, 4, 5, 6, 7, 8]}
      transformItem={(item, index, id, state) => {
        "worklet";
        paragraphBuilder.reset();
        return paragraphBuilder.addText(`Item ${item}`).build();
      }}
      renderItem={(item, index, state, element) => {
        "worklet";
        const { width } = state.layout.value;
        item.layout(width);
        const height = item.getHeight();
    
        if (!element) return height;
    
        element.addChild(
          SkiaDomApi.ParagraphNode({
            paragraph: item,
            x: 0,
            y: 0,
            width,
          }),
        );
    
        return height;
      }}
    />
  6. Use SkiaScrollViewState for manual transformations and redraws

    main

    The state object returned by useSkiaScrollView contains several properties for controlling the Skia canvas:

    • root: A SharedValue containing the root RenderNode<GroupProps>. You can transform the entire list by setting the matrix property on this node.
    • content: A SharedValue containing the content RenderNode<GroupProps> which holds the list items.
    • matrix: A SharedValue<SkMatrix> that translates the content node.
      • matrix.value[0] is the X scale.
      • matrix.value[4] is the Y scale.
      • matrix.value[5] is the Y translation (use scrollY instead).
      • matrix.value[2] is the X translation (use safeArea.left instead).
      • matrix.value[1] is the X skew.
      • matrix.value[3] is the Y skew.
    • redraw(): Call this to request a redraw of the Skia canvas (e.g., when adding a fixed element to the root node).
    • startedAnimation() / finishedAnimation(): Use these when animating properties to switch the rendering mode to "continuous" for efficient re-rendering.
    • pressing: A SharedValue<boolean> indicating if the view is currently being pressed.
    // Example: Transforming the entire list via the root node
    root.value.setProp("matrix", Skia.Matrix().skew(1, 0.5).get());
    
    // Example: Requesting a redraw
    state.redraw();
  7. How interpolateBackoff handles multi-segment ranges

    main

    interpolateBackoff allows you to define complex mapping behaviors using arrays for inputRange and outputRange.

    Requirements:

    • Both inputRange and outputRange must have at least 2 elements.
    • Both arrays must have the same length.

    Behavior:

    1. Within Range: The function iterates through the segments. If the value falls between inputRange[i] and inputRange[i+1], it uses interpolate to map it to the corresponding outputRange[i] and outputRange[i+1].
    2. Outside Range (Low): If value < inputRange[0], it uses the first segment (inputRange[0] to inputRange[1]) to calculate the backoff.
    3. Outside Range (High): If value > inputRange[last], it uses the last segment to calculate the backoff.

    Errors:

    • Throws Error("Input and output ranges must have at least 2 elements.") if arrays are too short.
    • Throws Error("Input and output ranges must be of the same length.") if lengths mismatch.
    // Example of multi-segment mapping
    // 0 -> 0
    // 5 -> 50
    // 10 -> 100
    // 12 -> 204 (backoff applied to distance 2: 100 + 2^2)
    const result = interpolateBackoff(12, [0, 5, 10], [0, 50, 100], 2);
  8. Use SkiaFlatList to render lists

    main

    Use <SkiaFlatList /> as a high-performance replacement for the standard React Native <FlatList />. It leverages the Skia Rendering Engine, which means you cannot use standard React Native components inside it; instead, you must use Skia drawing primitives.

    Key Requirements

    • Layout Calculation: You must manually calculate or provide the layout and dimensions of your list items. For text, you can use Skia's measureText() or the Paragraph getHeight() API.
    • Worklets: Functions like transformItem and renderItem must be marked with the "worklet" directive to run on the Skia thread.
    • Data Serialization: The initialData prop should return an array that can be serialized and passed to the worklet thread.

    Core Props

    • initialData: A function returning the initial array of data.
    • transformItem: A worklet function called once per item during the initial mount to transform the raw data into a Skia-compatible object.
    • renderItem: A worklet function called whenever an item's visibility changes. It is responsible for calculating the item's height and adding Skia nodes to the provided element.
    import { Skia } from "@shopify/react-native-skia";
    import { SkiaFlatList } from "react-native-skia-list";
    import { useSafeAreaInsets } from "react-native-safe-area-context";
    
    const paragraphBuilder = Skia.ParagraphBuilder.Make({
      textStyle: {
        color: Skia.Color("black"),
        fontSize: 20,
      },
    });
    
    export default function MyList() {
      const safeArea = useSafeAreaInsets();
    
      return (
        <SkiaFlatList
          safeArea={safeArea}
          initialData={() => [0, 1, 2, 3, 4, 5, 6, 7, 8]}
          transformItem={(item, index, id, state) => {
            "worklet";
            paragraphBuilder.reset();
            return paragraphBuilder.addText(`Item ${item}`).build();
          }}
          renderItem={(item, index, state, element) => {
            "worklet";
            const { width } = state.layout.value;
            item.layout(width);
            const height = item.getHeight();
    
            if (!element) return height;
    
            element.addChild(
              SkiaDomApi.ParagraphNode({
                paragraph: item,
                x: 0,
                y: 0,
                width,
              }),
            );
    
            return height;
          }}
        />
      );
    }
  9. Use the useSkiaFlatList hook

    main

    The useSkiaFlatList hook is the primary way to manage and access the state of a SkiaFlatList. It returns a state object that contains both the configuration and the imperative methods needed to control the list. You must pass this state to the SkiaFlatList component via the list prop.

    const state = useSkiaFlatList({
      data: myData,
      renderItem: ({ item }) => {
        // return height
        return 100;
      },
    });
    
    <SkiaFlatList list={state} style={{ flex: 1 }} />
    const state = useSkiaFlatList({ ... });
    
    <SkiaFlatList list={state} style={{ flex: 1 }} />
  10. Use SkiaFlatList for high-performance lists

    main

    The SkiaFlatList component is used to render high-performance lists using Skia. It relies on worklets for performance, so functions passed to its props must include the "worklet"; directive.

    Key props include:

    • initialData: A function that returns a serializable array to be passed to the worklet thread.
    • transformItem: A worklet function called once per item during the initial mount to prepare the item data.
    • renderItem: A worklet function called whenever an item's visibility changes. It is responsible for calculating layout and adding Skia nodes to the element.
    // needed for SkiaDomApi type
    import type {} from "@shopify/react-native-skia/lib/typescript/src/renderer/HostComponents";
    import { Skia } from "@shopify/react-native-skia";
    import { SkiaFlatList } from "react-native-skia-list";
    import { useSafeAreaInsets } from "react-native-safe-area-context";
    import { GestureHandlerRootView } from "react-native-gesture-handler";
    import { KeyboardProvider } from "react-native-keyboard-controller";
    
    // Create a Skia ParagraphBuilder that will be used to build the text for each item
    const paragraphBuilder = Skia.ParagraphBuilder.Make({
    	textStyle: {
    		color: Skia.Color("black"),
    		fontSize: 20,
    	},
    });
    
    export default function Test() {
    	const safeArea = useSafeAreaInsets();
    
    	return (
    		<KeyboardProvider>
    			<GestureHandlerRootView style={{ flex: 1 }}>
    				<SkiaFlatList
    					safeArea={safeArea}
    					// Provide an initialData array that can be serialized and passed to the worklet thread
    					initialData={() => [0, 1, 2, 3, 4, 5, 6, 7, 8]}
    					// To optimize performance for the initial mount you can provide a transformItem function
    					// It will be called once for each item when it is mounted the first time
    					transformItem={(item, index, id, state) => {
    						"worklet";
    
    						paragraphBuilder.reset(); // reuses the paragraphBuilder for each item
    						const text = `Item ${item}`;
    
    						return paragraphBuilder.addText(text).build();
    					}}
    					// renderItem will be called whenever an item visibility changes
    					renderItem={(item, index, state, element) => {
    						"worklet";
    
    						const { width } = state.layout.value;
    
    						item.layout(width); // calculates the paragraph layout
    
    						const height = item.getHeight(); // gets the height of the paragraph
    
    						// element is a Skia.GroupNode or will be undefined if only the height of the element is needed
    						if (!element) return height;
    
    						element.addChild(
    							// see the following link for all element types
    							// https://github.com/Shopify/react-native-skia/blob/5c38b27d72cea9c158290adb7d23c6109369ac2f/packages/skia/src/renderer/HostComponents.ts#L72-L191
    							SkiaDomApi.ParagraphNode({
    								paragraph: item,
    								x: 0,
    								y: 0,
    								width,
    							})
    						);
    
    						return height;
    					}
    					/>
    				</GestureHandlerRootView>
    			</KeyboardProvider>
    		);
    }
  11. Detect items at touch positions with getItemFromTouch

    main

    You can identify which item is at a specific coordinate using state.getItemFromTouch({ x, y }). This is useful for implementing custom interaction logic.

    Returns: A TapResult object containing details about the hit item, or undefined if no item was hit.

    TapResult Fields:

    • item: The data item (T).
    • id: Unique key of the item.
    • index: Index in the data array.
    • x / y: Left/Top position of the item relative to the screen.
    • rowY: Top position of the list row relative to content start.
    • touchX / touchY: The coordinates of the touch event.
    • height: Height of the list item.