React native: Ошибка: Обработано [больше/меньше] крючков, чем во время предыдущего рендеринга

#javascript #reactjs #react-native

Вопрос:

Поэтому я создал верхнюю липкую панель для отображения вкладок для разных категорий продуктов. Панель будет появляться и исчезать в зависимости от того, как далеко внизу экрана находится пользователь, а также будет меняться индикатор вкладки в соответствии с отображаемой категорией.

Это работает отлично и классно. За исключением случаев, когда пользователь опускает экран, чтобы обновить/обновить данные и количество изменений категории. Когда это происходит, приложение вылетает с этой ошибкой:

 Error: Rendered more** hooks than during the previous render.
 

** Если в новых данных меньше «вкладок», то ошибка отображается меньше, а не больше.

Я знаю, откуда берется ошибка, я просто не знаю, как ее исправить. Это происходит при создании refs для каждой вкладки с помощью useRef() крючка. При повторном рендеринге приложения (из-за обновления данных) и изменении количества подключений происходит сбой. Есть ли способ уничтожить предыдущие ссылки? У меня нет идей, и любое предложение будет высоко оценено.

Tabs.js — Функциональный компонент

 const Tabs = ({ tabs, tabIndex, show, onPress }) => {
    const tabsContainerRef = useRef(null);
    const tabsScrollViewRef = useRef(null);
    const indicatorRef = useRef(null);

    const translateY = useRef(new Animated.Value(-100)).current;
    const translateX = useRef(new Animated.Value(0)).current;
    const indicatorWidth = useRef(new Animated.Value(1)).current;

    const [hasLoaded, setHasLoaded] = useState(false);

    // THIS IS CAUSING THE ERROR !
    tabs = tabs.map((tab) => {
        return { ...tab, ref: useRef() };
    });

    useEffect(() => {
        if (!hasLoaded) {
            setHasLoaded(true);
        }
    }, []);

    useEffect(() => {
        if (hasLoaded) {
            tabs[0].ref?.current.measure((x, y, width, height) => {
            Animated.timing(indicatorWidth, {
                toValue: width,
                duration: 250,
                useNativeDriver: false,
            }).start();
            });
        }
    }, [hasLoaded]);

    useEffect(() => {
        Animated.timing(translateY, {
            toValue: show ? 0 : -100,
            duration: 250,
            useNativeDriver: true,
        }).start();
    }, [show]);

    useEffect(() => {
        setTimeout(() => {
            if (show) {
            tabs[tabIndex].ref?.current.measureLayout(
                tabsScrollViewRef.current,
                (x, y, width, height) => {
                // console.log(x, y, width, height);
                Animated.timing(indicatorWidth, {
                    toValue: width,
                    duration: 250,
                    useNativeDriver: false,
                }).start();

                Animated.timing(translateX, {
                    toValue: x,
                    duration: 250,
                    useNativeDriver: false,
                }).start();

                tabs[tabIndex].ref?.current.measure(
                    (tabX, tabY, tabWidth, tabHeight, tabPageX, tabPageY) => {
                    // console.log(tabX, tabY, tabWidth, tabHeight, tabPageX, tabPageY);
                    if (tabPageX < 0 || tabPageX > SCREEN_WIDTH)
                        tabsScrollViewRef.current.scrollTo({
                        x,
                        animated: true,
                        });
                    }
                );
                },
                (error) => {
                console.log("Failed to track tab indicator: ", error);
                }
            );
            }
        }, 250);
    }, [tabIndex]);

    return (
        <Animated.View
            ref={tabsContainerRef}
            style={[
            styles.containerTabs,
            {
                transform: [{ translateY }],
            },
            ]}
        >
            <ScrollView
            ref={tabsScrollViewRef}
            style={styles.containerTabsScrollView}
            contentContainerStyle={styles.contentTabsScrollView}
            showsHorizontalScrollIndicator={false}
            horizontal
            >
            {tabs.map((tab, index) => {
                return (
                <TouchableOpacity
                    ref={tab.ref}
                    key={index}
                    onPress={() => onPress(index)}
                    style={styles.containerTab}
                >
                    <Animated.View
                    style={
                        tabIndex === index
                        ? [styles.containerTabContent, { opacity: 1 }]
                        : styles.containerTabContent
                    }
                    >
                    <Text style={styles.textTab}>{tab.title}</Text>
                    </Animated.View>
                </TouchableOpacity>
                );
            })}

            <Animated.View
                ref={indicatorRef}
                style={{
                height: 5,
                backgroundColor: ClientConfig.colors.primary,
                position: "absolute",
                bottom: 0,
                width: indicatorWidth,
                transform: [{ translateX }],
                }}
            ></Animated.View>
            </ScrollView>
        </Animated.View>
    );
};
 

Дополнительный запрос

Компонент, вызывающий мой компонент Tabs , является типом компонента класса, могу ли я создать ссылки на вкладки с его помощью (с помощью React.createRef() ), отправить его на мои вкладки (функциональный компонент) и использовать его там? Я попытался отправить refs данные с вкладками через реквизит, и мне это не понравилось.

Спасибо.

Комментарии:

1. Будет ли иметь значение, если я сделаю его компонентом класса?? 🤔

Ответ №1:

После многих испытаний я нашел решение. Все, что мне нужно было сделать, это убедиться, что компонент был отключен при обновлении данных, чтобы все крючки были очищены React. Поэтому, когда я вызвал компонент Tabs , я добавил условный оператор !refreshing примерно так:

 {!refreshing amp;amp; tabs.length amp;amp; (
    <Tabs
        tabs={tabs}
        tabIndex={tabIndex}
        show={showTabs}
        onPress={this.onTabPress}
    />
)}
 

Это окончательно решило проблему :). Приведенный выше код для вкладок нуждался в некоторой тонкой настройке после этого изменения, ширина индикатора не устанавливается после обновления, но это нормально, легко исправить.