Animations stops after UI thread dip
Closed this issue · 4 comments
Is there an existing issue for this?
- I have searched the existing issues
Current Behavior
I see a dip in the UI thread and then it stops animating
Expected Behavior
firstly that the UI thread doesn't dip and if it does that it won't get stuck in the animation.
Another expected behavior is that I messed something up since I'm new to animations in React Native 🙈
Steps To Reproduce
LoadingSpinner
import { Feather } from '@expo/vector-icons'
import { MotiText } from 'moti'
import React from 'react'
import type { TextStyle } from 'react-native'
import { Easing } from 'react-native-reanimated'
import { Colors, Typography } from '~/constants/Theme'
type Props = {
color?: TextStyle['color']
size?: number
featherProps?: Partial<React.ComponentProps<typeof Feather>>
}
export function LoadingSpinner({ color = Colors['sand'], size = Typography.fontSize['md'] - 1, ...props }: Props) {
return (
<MotiText
from={{
rotate: '0deg',
}}
animate={{
rotate: '360deg',
}}
transition={{
loop: true,
repeatReverse: false,
type: 'timing',
easing: Easing.linear,
duration: 4000,
}}
>
<Feather
name="loader"
size={size}
style={{
paddingBottom: 5,
color,
}}
{...props.featherProps}
/>
</MotiText>
)
}
TextField
import { Feather } from '@expo/vector-icons'
import { MotiText, useAnimationState } from 'moti'
import React from 'react'
import { Text, View, TextInput, TextStyle, ViewStyle } from 'react-native'
import { styles, ClassNames } from './index.style'
import { LoadingSpinner } from '~/components/LoadingSpinner'
import { Colors } from '~/constants/Theme'
import { getTestId } from '~/test/getTestId/index'
type Props = {
label: string
placeholder?: string
value?: string
onChangeText?: TextInput['props']['onChangeText']
loading?: boolean
errorMessage?: string
locked?: boolean
testIdSuffix?: string
viewProps?: View['props'] & { style?: ViewStyle }
textProps?: Text['props'] & { style?: TextStyle }
textInputProps?: Omit<TextInput['props'], 'onChangeText' | 'value'> & {
style?: TextStyle
}
textInputAttributes?: React.ClassAttributes<TextInput>
}
export function TextField({ loading = false, ...props }: Props) {
const testId = (elementStyle: keyof ClassNames) =>
getTestId<ClassNames>('TextField', elementStyle, props.testIdSuffix ?? '')
const [hasFocus, setHasFocus] = React.useState<boolean | undefined>()
const revealLabelAnimation = useAnimationState({
show: {
opacity: 1,
top: 0,
},
hide: {
opacity: 0,
top: 20,
},
})
React.useEffect(() => {
if (hasFocus || props.value?.length) {
revealLabelAnimation.transitionTo('show')
} else {
revealLabelAnimation.transitionTo('hide')
}
}, [hasFocus, props.value, revealLabelAnimation])
const iconProps = React.useMemo(
() => ({
size: 20,
color: props?.textProps?.style?.color ?? styles.labelText.color,
}),
[props?.textProps?.style?.color]
)
return (
<View style={styles.container} {...props.viewProps} testID={testId('container')}>
<View style={styles.labelView} testID={testId('labelView')}>
<MotiText
state={revealLabelAnimation}
{...props.textProps}
style={[styles.labelText, props.textProps?.style]}
testID={testId('labelText')}
>
<Text>{props.label}</Text>
</MotiText>
{props.locked && <Feather name="lock" {...iconProps} />}
{loading && <LoadingSpinner featherProps={iconProps} />}
{!!props.errorMessage && (
<Feather name="alert-circle" {...iconProps} color={styles.validationErrorText.color} />
)}
</View>
<TextInput
accessibilityLabel={`${props.label} indtastningsfelt${props.locked ? ' (låst)' : ''}`}
accessibilityHint={`Indtast ${props.label} her`}
testID={testId('inputField')}
placeholder={!hasFocus ? props.label.toUpperCase() : ''}
placeholderTextColor={Colors['sand']}
value={props.value}
onFocus={() => setHasFocus(true)}
onBlur={() => setHasFocus(false)}
onChangeText={props.onChangeText}
editable={!props.locked}
selectTextOnFocus={!props.locked}
style={[
styles.inputField,
hasFocus && styles.inputFieldFocus,
props.locked && styles.inputFieldLocked,
!!props.errorMessage && styles.inputFieldValidationError,
]}
{...props.textInputProps}
{...props.textInputAttributes}
/>
{!!props.errorMessage && <Text style={styles.validationErrorText}>{props.errorMessage}</Text>}
</View>
)
}
Versions
- Moti: 0.17.1
- Reanimated: ~2.3.1
- React Native: 0.64.3
- expo: ~44.0.0
Screenshots
Reproduction
Create an via create-react-native-app
example here
The UI thread dip is likely due to frequent react renders. This should get fixed with transitions in react 18.
Try wrapping your sprinter in React.memo
?
are you using hasFocus
for anything other than animating? if not, you should just fire the animation state changes in onFocus
/onBlur
to avoid an extra render
The UI thread dip is likely due to frequent react renders. This should get fixed with transitions in react 18.
Try wrapping your sprinter in
React.memo
?
That worked like a charm 👍 thanks a ton!
export const LoadingSpinner = React.memo(
({ color = Colors['sand'], size = Typography.fontSize['md'] - 1, ...props }: Props) => {
return (
<MotiText
from={{
rotate: '0deg',
}}
animate={{
rotate: '360deg',
}}
transition={{
loop: true,
repeatReverse: false,
type: 'timing',
easing: Easing.linear,
duration: 4000,
}}
>
<Feather
name="loader"
size={size}
style={{
paddingBottom: 5,
color,
}}
{...props.featherProps}
/>
</MotiText>
)
}
)
are you using
hasFocus
for anything other than animating? if not, you should just fire the animation state changes inonFocus
/onBlur
to avoid an extra render
I'm using it to hide the placeholder when it gets focus - I'm very open to suggestions.