Skip to content

Glith on rendering new item #1864

@umardev500

Description

@umardev500

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",
        },
      },
    },
  },
}));

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions