question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

[v2.3+] SharedValue Height is not applied in some cases

See original GitHub issue

Description

Sometimes my Animated content is not reacting to Height changes ( only in v2.3+, works fine in v2.2 )

first i thought its Modal plugin proble so i made my own simple Modal Component to try it with expo and without expo ( ejected ) and it looks like its new Reanimated problem ( working on expo 43 with reanimated 2.2 )

  • this modal in on separated SCREEN “containedTransparentModal” from “@react-navigation

Expected behavior

when i set sharedValue i expect to reflect it on height

Actual behavior & steps to reproduce

  1. Open custom modal when i set HEIGHT from 0 to 500
  2. when i add space to code ( to force refresh ) its fixed …

Preview:

https://user-images.githubusercontent.com/53254371/149488127-2bdae745-7b0c-4476-8c6a-bcf958da4253.mp4

Snack or minimal code example

  • it requires Portal package @gorhom/portal ( but portal is not issue here )
<BottomModal
                id="orderFilter"
                visible={true}
                setVisible={() => {}} // useless cuz we are going BACK
                snapPoints={[Layout.window.height * .55, Layout.window.height * .90]}
                onAfterDismiss={() => {
                    navigation.goBack();
                }}
                title="test"
            ></BottomModal>
import React from "react";
import {BackHandler, Keyboard, Pressable, StyleSheet, Text, View, ViewStyle} from "react-native";
import Animated, {FadeIn, FadeOut, runOnJS, SlideInDown, useAnimatedGestureHandler, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'
import {Portal, PortalHost} from "@gorhom/portal";
import {PanGestureHandler} from "react-native-gesture-handler";
// import ModalTitle from "./ModalTitle";

type GestureHandlerContextType = { startHeight: number }

interface BottomModalProps {
    id: string // should be unique if there is multiple modals at same time
    visible: boolean
    setVisible: React.Dispatch<boolean>
    snapPoints: number[]

    title: string

    contentViewContainerStyle?: ViewStyle
    contentViewStyle?: ViewStyle

    onPanDownDismiss?: boolean // default - true

    backdropOptions?: {
        color?: string // default - rgba(2,2,2,0.5)
        dismissOnPress?: boolean // default - true
    }

    onDismiss?: () => void // this will fire before animation starts
    onAfterDismiss?: () => void // this will fire when animation finish, before setVisible is called
}

const BottomModal: React.FC<BottomModalProps> = (props) => {

    const currentSnapIndex = useSharedValue<number>(-1); // contentHeight 0
    const contentHeight = useSharedValue(0); // snap index -1

    const afterDismiss = () => {
        if (props.onAfterDismiss) {
            props.onAfterDismiss();
        }
        props.setVisible(false)
    }
    const dismiss = () => {
        Keyboard.dismiss();

        if (props.onDismiss) {
            props.onDismiss();
        }

        contentHeight.value = withTiming(0, undefined, (finished) => {
            runOnJS(afterDismiss)()
        })
    }

    const expand = () => {
        contentHeight.value = props.snapPoints[props.snapPoints.length - 1]
        currentSnapIndex.value = props.snapPoints.length - 1
    }

    const snapToClosest = () => {
        let currentSnapHeight = props.snapPoints[currentSnapIndex.value];

        /* end when its on same position */
        if (contentHeight.value === currentSnapHeight) {
            return;
        }

        if (contentHeight.value > currentSnapHeight) {
            // ++
            contentHeight.value = withTiming(props.snapPoints[currentSnapIndex.value + 1])
            currentSnapIndex.value += 1
        } else {
            // --
            if (typeof props.snapPoints[currentSnapIndex.value - 1] !== 'undefined') { // 0 => -1 only in specific situations
                contentHeight.value = withTiming(props.snapPoints[currentSnapIndex.value - 1])
                currentSnapIndex.value -= 1
            } else {
                // back to current snap
                contentHeight.value = withTiming(props.snapPoints[currentSnapIndex.value])
            }
        }
    }

    /* SNAPING / RESIZE handle */
    const onGestureHandler = useAnimatedGestureHandler({
        onStart(_, context: GestureHandlerContextType) {
            context.startHeight = contentHeight.value
        },
        onActive(event, context: GestureHandlerContextType) {

            let newVal = context.startHeight + (-1 * event.translationY);

            /* disable when overdrag last snapPoint */
            if (props.snapPoints[props.snapPoints.length - 1] < newVal) {
                return
            }

            contentHeight.value = newVal
        },
        onEnd(_, context: GestureHandlerContextType) {
            if (contentHeight.value < context.startHeight / 1.3) {
                if (typeof props.onPanDownDismiss === 'undefined' || props.onPanDownDismiss) {
                    runOnJS(dismiss)()
                } else {
                    if (!props.onPanDownDismiss) {
                        runOnJS(snapToClosest)()
                    }
                }
            } else {

                /* snap to closest snapPoint */
                runOnJS(snapToClosest)()
            }
        }
    })

    /* ANDROID back button handle */
    const backAction = () => {
        dismiss();
        return true;
    }
    React.useEffect(() => {
        BackHandler.addEventListener('hardwareBackPress', backAction)
        return () => BackHandler.removeEventListener("hardwareBackPress", backAction);
    }, [])

    /* SHOW / HIDE modal content */
    React.useEffect(() => {
        if (props.visible) {
            contentHeight.value = props.snapPoints[0]
            currentSnapIndex.value = 0
        } else {
            contentHeight.value = 0
            currentSnapIndex.value = -1
        }

        console.log('Content Visible - ', props.visible)
        console.log('Content height - ', contentHeight.value)
    }, [props.visible])

    const rContentStyle = useAnimatedStyle(() => {
        return {
            height: contentHeight.value
        }
    })

    if (!props.visible) {
        return null
    }

    return (
        <>
            <Portal name={props.id}>
                <Animated.View
                    entering={FadeIn}
                    exiting={FadeOut}
                    style={[styles.container, {backgroundColor: props.backdropOptions?.color ? props.backdropOptions.color : 'rgba(2,2,2,0.5)',}]}
                >
                    <Pressable style={{flex: 1}} onPress={() => {
                        if (props.backdropOptions?.dismissOnPress || typeof props.backdropOptions?.dismissOnPress === 'undefined') {
                            dismiss();
                            // expand()
                        }
                    }}/>

                    {/* contentContainer */}
                    <Animated.View
                        entering={SlideInDown}
                        style={[styles.contentContainer, props.contentViewContainerStyle, rContentStyle]}
                    >

                        {/* Header - SNAPING / RESIZE */}
                        <PanGestureHandler onGestureEvent={onGestureHandler}>
                            {/* TODO */}
                            <Animated.View style={{flexDirection: 'row', justifyContent: 'center', alignItems: 'center'}}>

                                {/*<ModalTitle title={props.title} dismiss={dismiss} />*/}

                                {/* spacer */}
                                <View style={{position: 'absolute', top: -10, backgroundColor: '#969696', width: 50, height: 5, borderRadius: 10}}/>
                            </Animated.View>
                        </PanGestureHandler>

                        {/* content */}
                        <View style={[{flex: 1}, props.contentViewStyle]}>
                            {props.children}
                        </View>
                    </Animated.View>
                </Animated.View>
            </Portal>
            <PortalHost name={props.id}/>
        </>
    )
}

const styles = StyleSheet.create({
    container: {
        position: 'absolute',
        height: '100%',
        width: '100%',
        justifyContent: 'flex-end'
    },
    contentContainer: {
        backgroundColor: 'white',
        borderRadius: 10,
    }
})

export default BottomModal

Package versions

name version
react-native 0.64.3
react-native-reanimated ~2.3.1
NodeJS v17.0.1
Xcode 13.2.1 (13C100)
expo 44

Affected platforms

  • Android
  • iOS

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:6 (2 by maintainers)

github_iconTop GitHub Comments

1reaction
piaskowykcommented, Jan 18, 2022

@hannojg right your case looks similar

1reaction
hannojgcommented, Jan 18, 2022

I have the feeling this could be related to this issue: https://github.com/software-mansion/react-native-reanimated/issues/2571 . Is that the case? (reproduction might be simpler there)

Read more comments on GitHub >

github_iconTop Results From Across the Web

min-height - CSS: Cascading Style Sheets - MDN Web Docs
The min-height CSS property sets the minimum height of an element. It prevents the used value of the height property from becoming smaller ......
Read more >
A Couple of Use Cases for Calc() | CSS-Tricks
Use Case #1: (All The Height – Header). A block level child element with height: 100% will be as tall as its block...
Read more >
Deep dive into React Native Reanimated - LogRocket Blog
We'll define a boxHeight variable as a Shared Value so that we can share it between the UI thread and the JavaScript thread...
Read more >
10 Visual formatting model details - W3C
Note: level 3 of CSS will probably include a property to select which measure of the font is used for the content height....
Read more >
Knowing Well, Being Well: well-being born of understanding ...
Creating Shared Value to Advance Racial Justice, Health Equity, ... B Corp is a non-profit network working to transform the global economy to...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found