diff --git a/fixture/react-native/src/Carousel.tsx b/fixture/react-native/src/Carousel.tsx index 68a719de2..a1a418a03 100644 --- a/fixture/react-native/src/Carousel.tsx +++ b/fixture/react-native/src/Carousel.tsx @@ -127,12 +127,13 @@ const Carousel = () => { useEffect(() => { if (isLayoutCompleteRef.current) { const targetIndex = lastVisibleIndexRef.current; - setTimeout(() => { + const timeout = setTimeout(() => { flatListRef.current?.scrollToIndex({ index: targetIndex, animated: false, }); }, 0); + return () => clearTimeout(timeout); } }, [screenWidth]); diff --git a/fixture/react-native/src/CellRendererExamples.tsx b/fixture/react-native/src/CellRendererExamples.tsx index 3be9776d3..381be6ee0 100644 --- a/fixture/react-native/src/CellRendererExamples.tsx +++ b/fixture/react-native/src/CellRendererExamples.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useCallback } from "react"; +import React, { forwardRef, useLayoutEffect, useRef, useCallback } from "react"; import { View, Text, StyleSheet, Animated } from "react-native"; import { FlashList } from "@shopify/flash-list"; @@ -7,57 +7,69 @@ import Tweet from "./twitter/models/Tweet"; import TweetCell from "./twitter/TweetCell"; // Example 1: Fade-in animation CellRendererComponent -const FadeInCellRenderer = (props: any) => { +const FadeInCellRenderer = forwardRef((props, ref) => { const opacity = useRef(new Animated.Value(0)).current; + const cellStyle = StyleSheet.flatten(props.style); - useEffect(() => { - Animated.timing(opacity, { + useLayoutEffect(() => { + opacity.setValue(0); + const animation = Animated.timing(opacity, { toValue: 1, duration: 500, delay: props.index * 100, useNativeDriver: true, - }).start(); + }); + animation.start(); + return () => animation.stop(); }, [opacity, props.index]); return ( ); -}; +}); +FadeInCellRenderer.displayName = "FadeInCellRenderer"; // Example 2: Scale animation CellRendererComponent -const ScaleCellRenderer = (props: any) => { +const ScaleCellRenderer = forwardRef((props, ref) => { const scale = useRef(new Animated.Value(0.8)).current; + const cellStyle = StyleSheet.flatten(props.style); - useEffect(() => { - Animated.spring(scale, { + useLayoutEffect(() => { + scale.setValue(0.8); + const animation = Animated.spring(scale, { toValue: 1, friction: 8, tension: 40, delay: props.index * 50, useNativeDriver: true, - }).start(); + }); + animation.start(); + return () => animation.stop(); }, [scale, props.index]); return ( ); -}; +}); +ScaleCellRenderer.displayName = "ScaleCellRenderer"; // Main component that combines both examples const FlashListCellRenderer = () => { diff --git a/fixture/react-native/src/List.tsx b/fixture/react-native/src/List.tsx index 5685798a1..e12ebbfc0 100644 --- a/fixture/react-native/src/List.tsx +++ b/fixture/react-native/src/List.tsx @@ -2,7 +2,13 @@ Use this component inside your React Native Application. A scrollable list with different item type */ -import React, { useCallback, useRef, useState } from "react"; +import React, { + forwardRef, + useCallback, + useEffect, + useRef, + useState, +} from "react"; import { View, Text, Pressable, StyleSheet } from "react-native"; import { FlashList, FlashListRef } from "@shopify/flash-list"; import Animated, { @@ -24,6 +30,17 @@ const List = () => { const [data, setData] = useState(() => generateArray(100)); const list = useRef | null>(null); + const refreshTimeout = useRef | null>(null); + + useEffect( + () => () => { + if (refreshTimeout.current !== null) { + clearTimeout(refreshTimeout.current); + refreshTimeout.current = null; + } + }, + [] + ); const removeItem = useCallback((item: number) => { list.current?.prepareForLayoutAnimationRender(); @@ -65,8 +82,10 @@ const List = () => { ref={list} refreshing={refreshing} onRefresh={() => { + if (refreshTimeout.current !== null) return; setRefreshing(true); - setTimeout(() => { + refreshTimeout.current = setTimeout(() => { + refreshTimeout.current = null; setRefreshing(false); }, 2000); }} @@ -85,16 +104,18 @@ const List = () => { export default List; -const CellRenderer = (props: any) => { +const CellRenderer = forwardRef((props, ref) => { return ( ); -}; +}); +CellRenderer.displayName = "CellRenderer"; const styles = StyleSheet.create({ container: { justifyContent: "space-around", diff --git a/fixture/react-native/src/components/RecyclingImage.tsx b/fixture/react-native/src/components/RecyclingImage.tsx index 1fba9e366..691e7bebb 100644 --- a/fixture/react-native/src/components/RecyclingImage.tsx +++ b/fixture/react-native/src/components/RecyclingImage.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useRef } from "react"; +import React, { useLayoutEffect, useRef } from "react"; import { Animated, Image, ImageProps, Platform } from "react-native"; interface RecyclingImageProps extends Omit { @@ -9,17 +9,17 @@ const isIOS = Platform.OS === "ios"; const RecyclingImageIOS = (props: RecyclingImageProps) => { const animatedOpacity = useRef(new Animated.Value(0)).current; - useMemo(() => { + useLayoutEffect(() => { animatedOpacity.setValue(0); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.source.uri]); + }, [animatedOpacity, props.source.uri]); return ( { + onLoad={(event) => { animatedOpacity.setValue(1); + props.onLoad?.(event); }} /> );