Skip to content

[Bug] Jarring layout shifts for each item using LayoutAnimation w/ inverted list #1596

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
1 of 2 tasks
kevinschaich opened this issue Apr 2, 2025 · 5 comments
Open
1 of 2 tasks
Labels
bug Something isn't working

Comments

@kevinschaich
Copy link

Current behavior

When new items are added to the list (inverted, so they get added at the bottom), every item jumps from its position first down, then slides up slowly to meet its final position.

Video:

Simulator.Screen.Recording.-.iPhone.16.Pro.Max.-.2025-04-02.at.12.27.23.mp4

Expected behavior

When a new item is added, they should slowly shift up from their current layout position to their final layout position without any "jumps"

To Reproduce

Full minimal repro is here:

https://github.com/kevinschaich/flash-list-layout-bug

Relevant code:

import { FlashList } from '@shopify/flash-list'
import * as React from 'react'
import { Button, LayoutAnimation, Text, TextInput, View, ViewStyle } from 'react-native'
import { KeyboardAvoidingView, KeyboardStickyView } from 'react-native-keyboard-controller'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { MOCK_MESSAGES, MockMessage } from '~/app/data'
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
    return twMerge(clsx(inputs))
}

export default function Chat() {
    const insets = useSafeAreaInsets()
    const [messages, setMessages] = React.useState(MOCK_MESSAGES)
    const flashListRef = React.useRef<FlashList<any>>(null)
    const [message, setMessage] = React.useState('')
    const ME = 'Alice'

    const addMessage = (newMessage: MockMessage) => {
        setMessages((prev) => [newMessage, ...prev])
        flashListRef.current?.prepareForLayoutAnimationRender()

        LayoutAnimation.configureNext({
            ...LayoutAnimation.Presets.linear,
            duration: 500, // Shorter duration to feel snappier
        })
    }

    function sendMessage() {
        addMessage({
            attachments: [],
            id: Math.random().toString(),
            reactions: {},
            sender: ME,
            text: message,
            newlySent: true,
            date: new Date().toISOString().split('T')[0],
            time: new Date().toLocaleTimeString('en-US', {
                hour: '2-digit',
                minute: '2-digit',
            }),
        })
        setMessage('')
    }

    return (
        <>
            <KeyboardAvoidingView
                style={[
                    {
                        flex: 1,
                        minHeight: 2,
                        backgroundColor: '#fff',
                    },
                ]}
                behavior='padding'
            >
                <FlashList
                    inverted
                    ref={flashListRef}
                    keyExtractor={(item) => item.id}
                    estimatedItemSize={70}
                    data={messages}
                    renderItem={({ item, index }) => {
                        const nextMessage = messages[index - 1]
                        const isSameNextSender =
                            typeof nextMessage !== 'string' ? nextMessage?.sender === item.sender : false

                        return (
                            <View
                                className={cn(
                                    'justify-center px-2 pb-3.5 sm:px-4',
                                    isSameNextSender ? 'pb-1' : 'pb-3.5',
                                    item.sender === ME ? 'items-end pl-16' : 'items-start pr-16',
                                )}
                            >
                                <View className='rounded-2xl bg-popover px-3 py-1.5 dark:bg-muted-foreground'>
                                    <Text>{item.text}</Text>
                                </View>
                            </View>
                        )
                    }}
                />
            </KeyboardAvoidingView>
            <KeyboardStickyView offset={{ opened: insets.bottom }}>
                <View className='w-full flex-row items-center gap-2 py-2 pb-8'>
                    <TextInput
                        placeholder='Message'
                        className='z-10 mr-4 flex-1 rounded-full border border-border bg-background px-4 py-2 text-lg leading-tight text-foreground sm:text-xl sm:leading-tight'
                        placeholderTextColor={'#333'}
                        multiline
                        onChangeText={setMessage}
                        value={message}
                    />
                    <Button onPress={sendMessage} title='send'></Button>
                </View>
            </KeyboardStickyView>
        </>
    )
}

Platform:

  • iOS
  • Android

Environment

1.8.0

@kevinschaich kevinschaich added the bug Something isn't working label Apr 2, 2025
@kevinschaich
Copy link
Author

kevinschaich commented Apr 2, 2025

I did manage to improve this by removing this line which was causing issues (transitions between two padding values, causing rest to shift):

isSameNextSender ? 'pb-1' : 'pb-3.5',

Improved the animation config a bit:

LayoutAnimation.configureNext({
    duration: 500,
    create: { type: 'linear', property: 'opacity' },
    update: { type: 'spring', springDamping: 10 },
    delete: { type: 'linear', property: 'opacity' },
})

But still one item (either most recent from the opposite sender, or two-line message, not sure) is jumping down first.

Simulator.Screen.Recording.-.iPhone.16.Pro.Max.-.2025-04-02.at.14.24.16.mp4

I pushed a new commit to the repo linked above with these changes.

@kevinschaich
Copy link
Author

Yeah OK narrowed it down to the issue. Items that are of a different size/larger than the rest are causing issues. If all the messages are one line, it works as intended, if there is even one that's two lines (and a thus larger height for that list element) we get jumps:

Simulator.Screen.Recording.-.iPhone.16.Pro.Max.-.2025-04-02.at.14.41.55.mp4

@zrina1314
Copy link

I am also using this component to make IM chat pages, but my layout is much more complicated than yours, I also have voice messages, picture messages, video messages, etc. The height is all uncertain.

@kevinschaich
Copy link
Author

I don't understand the reply here @zrina1314. Is it working for you with the layout transitions, or you're having the same issue as me?

@zrina1314
Copy link

#773

My app is similar to this one and has encountered the same problem as yours. I'm now looking forward to the official release of V2 and directly switch to V2 version

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants