javascriptreactjsreducersuse-reducer

Problem with reducer function in useReducer hook


The reducer function works fine except that previously added object element to the item array property gets over written whenever new object element is added. For example, if state.item contains {number: 1} and I add {number: 2}, it becomes [{number: 2},{number: 2}] instead of [{number: 1},{number: 2}].

The reducer function:

const reducer = (state, action) => {
  if (action.type === "ADD") {
    let newItem = state.item.concat(action.addItem);

    console.log("action.addItem:", action.addItem);
    console.log("state.item:", state.item);
    return { item: newItem };
  }
};

Is there any solution this problem?

Thank You.

Parent Component:

import React from "react";
import { useReducer } from "react";
import { CreateContext } from "./CreateContext";

const initialState = {
  item: [],
};

const reducer = (state, action) => {
  if (action.type === "ADD") {
    let newItem = state.item.concat(action.addItem);

    console.log("action.addItem:", action.addItem);
    console.log("state.item:", state.item);
    return { item: newItem };
  }
};

const AuthProvider = (props) => {
  // define useReducer
  const [state, dispatch] = useReducer(reducer, initialState);
  // define handlers
  const addItemHandler = (addItem) => {
    console.log("addItemHandler");
    dispatch({ type: "ADD", addItem: addItem });
  };
  const data = {
    addItem: addItemHandler,
    number: 0,
    item: state.item,
  };

  return (
    <CreateContext.Provider value={data}>
      {props.children}
    </CreateContext.Provider>
  );
};
export default AuthProvider;

Child component:

import React, { useState, useContext } from "react";
import {
  Button,
  Card,
  CardActionArea,
  CardActions,
  CardContent,
  CardMedia,
  makeStyles,
  Typography,
  Collapse,
  TextField,
  IconButton,
} from "@material-ui/core";
import clsx from "clsx";
import AddBoxIcon from "@material-ui/icons/AddBox";
import { Grid } from "@material-ui/core";
import { CreateContext } from "../Store/CreateContext";

const useStyles = makeStyles((theme) => ({
  card: {
    marginBottom: theme.spacing(5),
  },
  media: {
    height: 250,
    // smaller image for mobile
    [theme.breakpoints.down("sm")]: {
      height: 150,
    },
  },
  priceDetail: {
    marginLeft: theme.spacing(15),
  },
  numberTextField: {
    width: 52,
  },
  addBtn: {
    fontSize: 60,
  },
}));

const data = {
  id: null,
  name: null,
  price: null,
  quantity: null,
};

// img and title from the feed component
const Food = ({ img, title, description, price, id }) => {
  const classes = useStyles();
  // expand the description
  const [expanded, setExpanded] = React.useState(false);

  const handleExpandClick = () => {
    setExpanded(!expanded);
  };

  const newPrice = `RM${price.toFixed(2)} `;

  ////// process the form //////
  //get quantity from TextField
  const [quantity, setQuantity] = useState("");
  const quantityHandler = (enteredQuantity) => {
    console.log("enteredQuantity:", enteredQuantity);
    setQuantity(enteredQuantity.target.value);
  };
  // use useContext
  const AuthData = useContext(CreateContext);

  const submitHandler = (e) => {
    console.log("submit is pressed");
    e.preventDefault();
    data.id = id;
    data.title = title;
    console.log("data.id:", data.id);
    data.price = price;
    console.log("data.price:", data.price);
    data.quantity = quantity;
    console.log("quantity:", quantity);
    AuthData.addItem(data);
    console.log("AuthData:", AuthData.number);
  };

  return (
    <Grid item xs={12} md={6}>
      <form onSubmit={submitHandler}>
        <Card className={classes.card} id={id}>
          <CardActionArea>
            <CardMedia className={classes.media} image={img} title="My Card" />
            <CardContent>
              <Typography gutterBottom variant="h5">
                {title}
              </Typography>
              <Button
                size="small"
                color="primary"
                className={clsx(classes.expand, {
                  [classes.expandOpen]: expanded,
                })}
                onClick={handleExpandClick}
                aria-expanded={expanded}
                aria-label="show more"
              >
                Learn More
              </Button>
              <CardActionArea>
                <Collapse in={expanded} timeout="auto" unmountOnExit>
                  <CardContent>
                    <Typography paragraph>{description}</Typography>
                  </CardContent>
                </Collapse>
              </CardActionArea>
            </CardContent>
          </CardActionArea>

          <CardActions>
            {" "}
            <Typography variant="h6" className={classes.priceDetail}>
              {newPrice}
            </Typography>
            <Typography variant="h6" className={""}>
              x
            </Typography>
            <TextField
              id={id}
              label="amount"
              type="number"
              // value={}
              // onChange={}
              className={classes.numberTextField}
              label=""
              variant="outlined"
              min="1"
              max="5"
              step="1"
              defaultValue="0"
              size="small"
              onChange={quantityHandler}
              input={id}
              // ref={quantity}
            />
            <IconButton aria-label="" onClick={""} type="submit">
              <AddBoxIcon
                color="secondary"
                className={classes.addBtn}
              ></AddBoxIcon>
            </IconButton>
          </CardActions>
        </Card>
      </form>
    </Grid>
  );
};

export default Food;

Solution

  • The problem is entirely in your child component, and nothing to do with your reducer. It's how you are passing in the data that becomes the addItem payload in the action you dispatch.

    I have reproduced below the relevant parts of the child component (or rather the whole module), so you can see the problem more clearly:

    const data = {
      id: null,
      name: null,
      price: null,
      quantity: null,
    };
    
    const Food = ({ img, title, description, price, id }) => {
      // more code that isn't relevant here
    
      const submitHandler = (e) => {
        console.log("submit is pressed");
        e.preventDefault();
        data.id = id;
        data.title = title;
        console.log("data.id:", data.id);
        data.price = price;
        console.log("data.price:", data.price);
        data.quantity = quantity;
        console.log("quantity:", quantity);
        AuthData.addItem(data);
        console.log("AuthData:", AuthData.number);
      };
    
      // more code that isn't relevant here
    }
    

    The data that you are passing to AuthData.addItem (that ends up being passed to the reducer) isn't a new object each time - it's a single "global" (module-level) constant that you simply mutate each time you use it. This is what's causing your state to mutate - because each object that ends up in the array of your state.items is a reference to that same object, so each mutation you make (inside submitHandler) ends up changing every copy!

    You can easily fix this in the child component, by not simply mutating the same object each time but recreating a new one. I don't see any reason for what you are doing, so simply stop doing it! I would rewrite it as below:

    // note NO const data = ...
    
    const Food = ({ img, title, description, price, id }) => {
      // more code that isn't relevant here
    
      const submitHandler = (e) => {
        console.log("submit is pressed");
        e.preventDefault();
        const data = {};
        data.id = id;
        data.title = title;
        console.log("data.id:", data.id);
        data.price = price;
        console.log("data.price:", data.price);
        data.quantity = quantity;
        console.log("quantity:", quantity);
        AuthData.addItem(data);
        console.log("AuthData:", AuthData.number);
      };
    
      // more code that isn't relevant here
    }
    

    Doing it this way, each object in the state "item" array will be unique, and you can dispatch new ones without affecting the old ones!