Shopify / Shopify/flash-list

Glith on rendering new item

Open
#1,864 1 comment 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
TypeScript
Stars
7.2k
Forks
393
Avg merge
1d 3h
Merged PRs (30d)
1

Description

Current behavior

Currently when I added item to top then never rendered first time that with glitch on scrolling

Expected behavior

smooth scrolling while rendering new item never rendered before

To Reproduce

Already add keyExtractor and more

Platform:

  • iOS
  • Android

Environment

 "dependencies": {
    "@lodev09/react-native-true-sheet": "^2.0.6",
    "@react-native-vector-icons/icomoon": "^12.2.0",
    "@react-native/new-app-screen": "0.80.2",
    "@react-navigation/bottom-tabs": "^7.4.6",
    "@react-navigation/elements": "^2.6.3",
    "@react-navigation/native": "^7.1.17",
    "@react-navigation/native-stack": "^7.3.25",
    "@shopify/flash-list": "^2.0.3",
    "clsx": "^2.1.1",
    "formik": "^2.4.6",
    "nativewind": "^4.1.23",
    "react": "19.1.0",
    "react-native": "0.80.2",
    "react-native-edge-to-edge": "^1.6.2",
    "react-native-flash-message": "^0.4.2",
    "react-native-image-picker": "^8.2.1",
    "react-native-keyboard-controller": "^1.18.5",
    "react-native-reanimated": "^3",
    "react-native-safe-area-context": "^5.6.1",
    "react-native-screens": "^4.15.2",
    "yup": "^1.7.0"
  },
List
import { ChatListing } from "@/src/features/chat/components/molecule";
import { ChatMessage } from "@/src/features/chat/types";
import { faker } from "@faker-js/faker";
import { FlashList, FlashListRef, ListRenderItem } from "@shopify/flash-list";
import React, { useState } from "react";
import { useSafeAreaInsets } from "react-native-safe-area-context";

// Generate initial 50 dummy items
const generateItems = (start: number, count: number) => {
  const timestamp = faker.date.recent({ days: 7 });

  return Array.from({ length: count }, (_, i) => ({
    id: String(start + i),
    text: faker.lorem.sentence(),
    sender: Math.random() > 0.5 ? "u1" : "u2",
    timestamp: timestamp.toISOString(),
  }));
};

const CURRENT_USER = "u1";

interface Props {
  ref?: React.RefObject<FlashListRef<ChatMessage> | null>;
}

export const ChatList: React.FC<Props> = ({ ref }) => {
  const [items, setItems] = useState<ChatMessage[]>(generateItems(1, 50));
  const { bottom } = useSafeAreaInsets();

  const timestamp = faker.date.recent({ days: 7 });

  const handleStartReached = async () => {
    // Generate 15 new messages
    const newMessages = Array.from({ length: 115 }, (_, i) => ({
      id: `${Date.now()}-${i}`, // unique ID
      text: faker.lorem.sentence(),
      sender: Math.random() > 0.5 ? "u1" : "u2",
      timestamp: timestamp.toISOString(),
    }));

    // Prepend them to the existing items
    setItems((prev) => [...newMessages, ...prev]);
  };

  const renderItem: ListRenderItem<ChatMessage> = ({ index, item }) => {
    const prevUser = index > 0 ? items[index - 1].sender : null;
    const nextUser = index < items.length - 1 ? items[index + 1].sender : null;

    const prevIsMe = prevUser === item.sender;
    const nextIsMe = nextUser === item.sender;

    return (
      <ChatListing
        prevIsMe={prevIsMe}
        nextIsMe={nextIsMe}
        isMe={item.sender === CURRENT_USER}
        message={item}
      />
    );
  };

  return (
    <FlashList
      onStartReachedThreshold={0.6}
      onStartReached={handleStartReached}
      ref={ref}
      data={items}
      showsVerticalScrollIndicator
      renderItem={renderItem}
      keyExtractor={(item) => item.id}
      contentContainerStyle={{ paddingTop: 16, paddingBottom: bottom + 16 }}
      maintainVisibleContentPosition={{
        autoscrollToBottomThreshold: 0.2,
        startRenderingFromBottom: true, // keep chat starting from bottom
      }}
      getItemType={(item) => {
        return item.sender;
      }}
      drawDistance={600}
    />
  );
};

  • Card
import { ChatMessage } from "@/src/features/chat/types";
import colors from "@/src/theme/colors";
import { Image } from "expo-image";
import React, { useState } from "react";
import { Text, View } from "react-native";
import { StyleSheet } from "react-native-unistyles";

interface Props {
  message: ChatMessage;
  isMe: boolean;
  prevIsMe?: boolean;
  nextIsMe?: boolean;
}

const formatTime = (isoTimestamp: string): string => {
  return new Date(isoTimestamp).toLocaleTimeString([], {
    hour: "2-digit",
    minute: "2-digit",
    hour12: true,
  });
};

const MESSAGE_AVATAR_SIZE = 25;
const GAP_BETWEEN_AVATAR_AND_CARD = 8;
const GAP_BETWEEN_CARD_AND_TIME = 4;

export const ChatListing: React.FC<Props> = ({
  message,
  isMe,
  prevIsMe,
  nextIsMe,
}) => {
  const [showTime, setShowTime] = useState(false);

  styles.useVariants({
    isMe: isMe ? "true" : "false",
    prevIsMe: prevIsMe ? "true" : "false",
    nextIsMe: nextIsMe ? "true" : "false",
  });

  return (
    <View style={[styles.container]}>
      {!isMe && !nextIsMe && (
        <Image
          style={{ width: 25, height: 25 }}
          source={require("@/assets/images/avatar.png")}
          cachePolicy={"memory-disk"}
        />
      )}

      <View style={styles.inner}>
        <View style={[styles.card]}>
          <Text style={styles.text}>{message.text}</Text>
        </View>
        {showTime ||
          (!nextIsMe && (
            <Text style={styles.time}>{formatTime(message.timestamp)}</Text>
          ))}
      </View>
    </View>
  );
};

const styles = StyleSheet.create(() => ({
  container: {
    flex: 1,
    flexDirection: "row",
    gap: GAP_BETWEEN_AVATAR_AND_CARD,
    paddingHorizontal: 16,
    variants: {
      isMe: {
        true: {
          maxWidth: "65%",
          alignSelf: "flex-end",
        },
        false: {
          maxWidth: "70%",
          alignSelf: "flex-start",
          alignItems: "flex-end",
        },
      },
      prevIsMe: {
        true: {
          paddingTop: 4,
        },
        false: {
          paddingTop: 16,
        },
      },
    },
  },
  inner: {
    gap: GAP_BETWEEN_CARD_AND_TIME,
    variants: {
      nextIsMe: {
        true: {
          paddingLeft: MESSAGE_AVATAR_SIZE + GAP_BETWEEN_AVATAR_AND_CARD,
        },
        false: {},
      },
    },
  },
  card: {
    paddingHorizontal: 12,
    paddingVertical: 8,
    borderTopLeftRadius: 12,
    borderTopRightRadius: 12,
    variants: {
      isMe: {
        true: {
          backgroundColor: colors.blue[600],
          borderBottomLeftRadius: 12,
        },
        false: {
          backgroundColor: colors.gray[200],
          borderBottomRightRadius: 12,
        },
      },
    },
  },
  text: {
    fontSize: 15,
    lineHeight: 20,
    variants: {
      isMe: {
        true: {
          color: colors.white,
        },
        false: {
          color: colors.black,
        },
      },
    },
  },
  time: {
    fontSize: 12,
    color: colors.gray[500],
    variants: {
      isMe: {
        true: {
          alignSelf: "flex-end",
        },
        false: {
          alignSelf: "flex-start",
        },
      },
    },
  },
}));

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the supplied ChatList entry point and reproduce the iOS behavior using FlashList 2.0.3 with the shown prepend logic and maintainVisibleContentPosition settings. Compare the first render and scrolling after new items are prepended; done means newly added items render on the first attempt without the reported scrolling glitch.

Written by the indexing model from the issue text.

Assessment

Tech stack
react-native, typescript
Domain
mobile
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
32/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.