简体   繁体   中英

Why the state of a useState snaps back to false if I go back and forth on the Screen?

I've been trying to toggle favorite products and store their id and an isFav prop on a Firebase database. Then I use them to show the favorites on the FavoritesScreen .

If I go to the ProductDetailsScreen (where I toggle the favorite) I toggle it true/false with no problem.

Further if I then use the Bottom Tab Navigation to check the FavoritesScreen or the OrdersScreen etc and then go back to ProductDetailsScreen , nothing is changed.

But if (from the ProductDetailsScreen ) I go back (to ProductsOverviewScreen ) and then come back again on ProductDetailsScreen the state of isFav snaps back to false! Nevertheless the id and isFav are saved on Firebase, but isFav is saved as false .

Note: I use a useState() hook...

One more thing that I don't understand happens when I try to log isFav . I have two logs, one inside the toggleFavoriteHandler and one outside. When I first run the toggleFavoriteHandler , where I also have setIsFav(prevState =>;prevState); I get: Output:

outside: false inside: false outside: true

So I guess the first two false are from the initial state and then the true is from the above state-toggling. But why it gets it only outside true? Why actually the first two are false? I change the state to true before the log. I would expect it to immediately change to true and have them all true!

Then if I go back to ProductsOverviewScreen and then again to ProductDetailsScreen I get two logs from outside:

Output:

outside: true outside: false

So it snaps back to its initial state? ?

I really do not understand how the work-flow goes. Are these logs normal?

Can anybody give some hints where the bug from going back and forth could be, please?

Thanks!

Here is the code:

ProductDetailsScreen.js

...

const ProductDetailScreen = (props) => {
    const [ isFav, setIsFav ] = useState(false);
    const dispatch = useDispatch();

    const productId = props.navigation.getParam('productId');
    const selectedProduct = useSelector((state) =>
        state.products.availableProducts.find((prod) => prod.id === productId)
    );



    const toggleFavoriteHandler = useCallback(
        async () => {
            setError(null);
            setIsFav((prevState) => !prevState);
            console.log('isFav inside:', isFav); // On first click I get: false
            try {
                await dispatch(
                    productsActions.toggleFavorite(
                        productId,
                        isFav,
                    )
                );
            } catch (err) {
                setError(err.message);
            }
        },
        [ dispatch, productId, isFav setIsFav ]
    );
    console.log('isFav outside: ', isFav); // On first click I get: false true

    return (
        <ScrollView>
            <View style={styles.icon}>
                <TouchableOpacity style={styles.itemData} onPress={toggleFavoriteHandler}>
                    <MaterialIcons name={isFav ? 'favorite' : 'favorite-border'} size={23} color="red" />
                </TouchableOpacity>
            </View>
            <Image style={styles.image} source={{ uri: selectedProduct.imageUrl }} />
            {Platform.OS === 'android' ? (
                <View style={styles.button}>
                    <CustomButton
                        title="Add to Cart"
                        onPress={() => dispatch(cartActions.addToCard(selectedProduct))}
                    />
                </View>
            ) : (
                <View style={styles.button}>
                    <Button
                        color={Colours.gr_brown_light}
                        title="Add to Cart"
                        onPress={() => dispatch(cartActions.addToCard(selectedProduct))}
                    />
                </View>
            )}

            <Text style={styles.price}>€ {selectedProduct.price.toFixed(2)}</Text>
            <Text style={styles.description}>{selectedProduct.description}</Text>
        </ScrollView>
    );
};

ProductDetailScreen.navigationOptions = ({ navigation }) => {
    return {
        headerTitle: navigation.getParam('productTitle'),
        headerLeft: (
            <HeaderButtons HeaderButtonComponent={CustomHeaderButton}>
                <Item
                    title="goBack"
                    iconName={Platform.OS === 'android' ? 'md-arrow-back' : 'ios-arrow-back'}
                    onPress={() => navigation.goBack()}
                />
            </HeaderButtons>
        ),
        headerRight: (
            <HeaderButtons HeaderButtonComponent={CustomHeaderButton}>
                <Item
                    title="cart"
                    iconName={Platform.OS === 'android' ? 'md-cart' : 'ios-cart'}
                    onPress={() => navigation.navigate({ routeName: 'Cart' })}
                />
            </HeaderButtons>
        )
    };
};
...styles

products.js/actions

export const toggleFavorite = (id, isFav) => {
    return async (dispatch) => {
        try {
            // If it is a favorite, post it.
            // Note it is initially false... 
            if (!isFav) {
                const response = await fetch('https://ekthesi-7767c.firebaseio.com/favorites.json', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({
                        id,
                        isFav
                    })
                });

                if (!response.ok) {
                    throw new Error(
                        'Something went wrong.'
                    );
                }
                const resData = await response.json();

                // Note: No `name` property, that's why we use a `for_in` loop
                // console.log('POST', JSON.stringify(resData));

                dispatch({ type: TOGGLE_FAVORITE, productId: id });
            } else if (isFav) {
                // First get the key in order to delete it in second fetch(...).
                const response = await fetch(`https://ekthesi-7767c.firebaseio.com/favorites.json`);

                if (!response.ok) {
                    throw new Error(
                        'Something went wrong.'
                    );
                }

                const resData = await response.json();

                // Note: No `name` property, that's why we use a `for_in` loop
                // console.log('fetch', JSON.stringify(resData));

                for (const key in resData) {
                    console.log('resData[key].id', resData[key].id === id);
                    if (resData[key].id === id) {
                        await fetch(`https:app.firebaseio.com/favorites/${key}.json`, {
                            method: 'DELETE'
                        });

                        if (!response.ok) {
                            throw new Error(
                                'Something went wrong.'
                            );
                        }
                        // console.log('fetch', JSON.stringify(resData));
                        dispatch({ type: TOGGLE_FAVORITE, productId: id });
                    }
                }
            }
        } catch (err) {
            // send to custom analytics server
            throw err;
        }
    };
};

ProductsOverviewScreen.js

...

const ProductsOverviewScreen = (props) => {
    const [ isLoading, setIsLoading ] = useState(false);
    const [ error, setError ] = useState(); // error initially is undefined!
    const [ isRefresing, setIsRefresing ] = useState(false);
    const dispatch = useDispatch();
    const categoryId = props.navigation.getParam('categoryId');
    const products = useSelector((state) =>
        state.products.availableProducts.filter((prod) => prod.categoryIds.indexOf(categoryId) >= 0)
    );
    const productId = props.navigation.getParam('productId');
    const isFav = useSelector((state) => state.products.favoriteProducts.some((product) => product.id === productId));


    const loadProducts = useCallback(
        async () => {
            setError(null);
            setIsRefresing(true);
            try {
                await dispatch(productsActions.fetchProducts());
            } catch (err) {
                setError(err.message);
            }
            setIsRefresing(false);
        },
        [ dispatch, setIsLoading, setError ]
    );

    // loadProducts after focusing
    useEffect(
        () => {
            const willFocusEvent = props.navigation.addListener('willFocus', loadProducts);
            return () => willFocusEvent.remove();
        },
        [ loadProducts ]
    );

    // loadProducts initially...
    useEffect(
        () => {
            setIsLoading(true);
            loadProducts();
            setIsLoading(false);
        },
        [ dispatch, loadProducts ]
    );

    const selectItemHandler = (id, title) => {
        props.navigation.navigate('DetailScreen', {
            productId: id,
            productTitle: title,
            isFav: isFav
        });
    };

    if (error) {
        return (
            <View style={styles.centered}>
                <Text>Something went wrong!</Text>
                <Button title="Try again" onPress={loadProducts} color={Colours.chocolate} />
            </View>
        );
    }

    if (isLoading) {
        return (
            <View style={styles.centered}>
                <ActivityIndicator size="large" color={Colours.chocolate} />
            </View>
        );
    }

    if (!isLoading && products.length === 0) {
        return (
            <View style={styles.centered}>
                <Text>No products yet!</Text>
            </View>
        );
    }

    return (
        <FlatList
            onRefresh={loadProducts}
            refreshing={isRefresing}
            data={products}
            keyExtractor={(item) => item.id}
            renderItem={(itemData) => (
                <ProductItem
                    title={itemData.item.title}
                    image={itemData.item.imageUrl}
                    onSelect={() => selectItemHandler(itemData.item.id, itemData.item.title)}
                >
                    {Platform.OS === 'android' ? (
                        <View style={styles.actions}> 
                            <View>
                                <CustomButton
                                    title="Details"
                                    onPress={() => selectItemHandler(itemData.item.id, itemData.item.title)}
                                />
                            </View>
                            <BoldText style={styles.price}>€ {itemData.item.price.toFixed(2)}</BoldText>
                            <View>
                                <CustomButton
                                    title="Add to Cart"
                                    onPress={() => dispatch(cartActions.addToCard(itemData.item))}
                                />
                            </View>
                        </View>
                    ) : (
                        <View style={styles.actions}>
                            <View style={styles.button}>
                                <Button
                                    color={Colours.gr_brown_light}
                                    title="Details"
                                    onPress={() => selectItemHandler(itemData.item.id, itemData.item.title)}
                                />
                            </View>
                            <BoldText style={styles.price}>€ {itemData.item.price.toFixed(2)}</BoldText>
                            <View style={styles.button}>
                                <Button
                                    color={Colours.gr_brown_light}
                                    title="Add to Cart"
                                    onPress={() => dispatch(cartActions.addToCard(itemData.item))}
                                />
                            </View>
                        </View>
                    )}
                </ProductItem>
            )}
        />
    );
};

ProductsOverviewScreen.navigationOptions = (navData) => {
    return {
        headerTitle: navData.navigation.getParam('categoryTitle'),
        headerRight: (
            <HeaderButtons HeaderButtonComponent={CustomHeaderButton}>
                <Item
                    title="cart"
                    iconName={Platform.OS === 'android' ? 'md-cart' : 'ios-cart'}
                    onPress={() => navData.navigation.navigate({ routeName: 'Cart' })}
                />
            </HeaderButtons>
        )
    };
};
...styles

State updates are not synchronous. Considering the following:

const [isFav, setIsFav] = React.useState(true);

setIsFav(false); // state update here
console.log(isFav); // isFav hasn't updated yet and won't be `false` until next render

To get the latest state, you need to put your log in useEffect / useLayoutEffect .

From React docs,

Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.

setState() does not always immediately update the component. It may batch or defer the update until later.

https://reactjs.org/docs/react-component.html#setstate

After the comment of @satya I gave it another try. Now I get the state of isFav from the redux state. Namely, I check if the current product is in the favoriteProducts array.

...imports

const ProductDetailScreen = (props) => {
    const [ error, setError ] = useState(); // error initially is undefined!

    const dispatch = useDispatch();

    const productId = props.navigation.getParam('productId');
    const selectedProduct = useSelector((state) =>
        state.products.availableProducts.find((prod) => prod.id === productId)
    );
// HERE !!! I get to see if current product is favorite!
    const currentProductIsFavorite = useSelector((state) => state.products.favoriteProducts.some((product) => product.id === productId));


    const toggleFavoriteHandler = useCallback(
        async () => {
            setError(null);
            try {
                await dispatch(productsActions.toggleFavorite(productId, currentProductIsFavorite));
            } catch (err) {
                setError(err.message);
            }
        },
        [ dispatch, productId, currentProductIsFavorite, setIsFav ]
    );

    ...

    return (
        <ScrollView>
            <View style={styles.icon}>
                <TouchableOpacity style={styles.itemData} onPress={toggleFavoriteHandler}>
                    <MaterialIcons name={currentProductIsFavorite ? 'favorite' : 'favorite-border'} size={23} color="red" />
                </TouchableOpacity>
            </View>
            <Image style={styles.image} source={{ uri: selectedProduct.imageUrl }} />
    ...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM