Ray
Ray

Reputation: 63

React hooks setstate change only reflects first change

I'm trying to have a food filter page whereby the user presses buttons to indicate their preference so that on confirm, I would be able to have the user's preference stored as on object with keys "area", "cuisine" and "price". After trying various combinations, the logged object I get keeps only reflecting a change in the first update which is area. {"area": "South", "cuisine": Array [], "price": -Infinity } instead of {"area":"South", "cuisine": "Japanese Cuisine", "price": 3}. How do I deal with this async nature of setstate to get all the updates correct? (the handle press functions are passed to children component for area cuisine and price)

enter image description here

Filter.js

const [filters, setFilters] = React.useState({});

    React.useEffect(() => {
        setFilters({ area: "", cuisine: "", price: 0 });
    }, []);

    const handleAreaPress = (area) => {
        setFilters((prevState) => ({ ...prevState, area: area }));
    };

    const handleCuisinePress = (cuisine) => {
        setFilters((prevState) => ({ ...prevState, cuisine: cuisine }));
    };

    const handlePricePress = (price) => {
        let max = Math.max(...price);
        setFilters((prevState) => ({ ...prevState, price: max }));
    };

    const handleConfirmPress = () => {
        console.log(filters);  // expected log to be {"area":"South", "cuisine": "Japanese Cuisine", "price": 3}
    };

Area.js

const AreaSelection = ({ handleAreaPress }) => {
    const [selected, setSelected] = React.useState([]);
    const areas = ["North", "South", "East", "West", "Central"];

    const handlePress = (area) => {
        setSelected(area);
    };

    const buttons = () =>
        areas.map((items) => (
            <TouchableOpacity
                key={items}
                onPress={() => {
                    handlePress(items);
                    handleAreaPress(items);
                }}
                style={[
                    styles.button,
                    {
                        backgroundColor: selected.includes(items)
                            ? "silver"
                            : "white",
                    },
                ]}
            >

Cuisine.js

const CuisineSelection = ({ handleCuisinePress }) => {
    const [selected, setSelected] = React.useState([]);
    const cuisine = [
        "Asian",
        "Western",
        "Chinese",
        "Korean",
        "Indian",
        "Japanese",
        "Cafe",
        "Local",
    ];

    const handlePress = (cuisine) => {
        selected.includes(cuisine)
            ? setSelected(selected.filter((s) => s !== cuisine))
            : setSelected([...selected, cuisine]);
    };

    const buttons = () =>
        cuisine.map((items) => (
            <TouchableOpacity
                key={items}
                onPress={() => {
                    handlePress(items);
                    handleCuisinePress(selected);
                }}
                style={[
                    styles.button,
                    {
                        backgroundColor: selected.includes(items)
                            ? "silver"
                            : "white",
                    },
                ]}
            >

Upvotes: 0

Views: 498

Answers (3)

Yu Mao
Yu Mao

Reputation: 101

 const handlePress = (cuisine) => {
    let newSelected = [];

    if (selected.includes(cuisine)) {
      newSelected = selected.filter((s) => s !== cuisine);
    } else {
      newSelected = selected.concat(cuisine);
    }

    setSelected(cuisine);
    handleCuisinePress(newSelected);
};

The selected state you pass to handleCuisinePress is not up to date, you shouldn't rely on it, just pass the new state you calculated in handlePress to handleCuisinePress

Upvotes: 1

Clarity
Clarity

Reputation: 10873

The area code is actually working correct. The issue is in this code where you call two functions setting state:

    handlePress(items);
    handleCuisinePress(selected);

handlePress sets the internal component state, and then handleCuisinePress reads that state and sets it on the filters. However, since setting state is async when handleCuisinePress is called, it uses the old values of state since the handlePress results aren't applied yet.

The best way to handle this wold be to get rid of internal component state and read it directly from the filters:

// Declare array outside of component to not create it on every render
const cuisine = [
        "Asian",
        "Western",
        "Chinese",
        "Korean",
        "Indian",
        "Japanese",
        "Cafe",
        "Local",
    ];

const CuisineSelection = ({ handleCuisinePress, cuisine }) => {
    const buttons = () =>
        cuisine.map((items) => (
            <TouchableOpacity
                key={items}
                onPress={() => {
                    handleCuisinePress(items);
                }}

            >

Upvotes: 1

sebastian-ruehmann
sebastian-ruehmann

Reputation: 503

You can define an expected value range and conditionally fallback to a default value.

    const handlePricePress = (price) => {
        const max = price >= 0 ? Math.max(...price) : 0;
        setFilters((prevState) => ({ ...prevState, price: max }));
    };

Upvotes: 0

Related Questions