useReducer hook 中reducer 函数的问题
Problem with reducer function in useReducer hook
reducer 函数工作正常,除了先前添加到 item
数组 属性 的对象元素在添加新对象元素时会被覆盖。
例如,如果 state.item
包含 {number: 1}
并且我添加 {number: 2}
,它变成 [{number: 2},{number: 2}]
而不是 [{number: 1},{number: 2}]
。
reducer函数:
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 };
}
};
这个问题有解决办法吗?
谢谢。
父组件:
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;
子组件:
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;
问题完全出在你的子组件上,与你的reducer无关。这就是您传递数据的方式,这些数据成为您发送的操作中的 addItem
有效负载。
我已经将子组件(或者说是整个模块)的相关部分转载如下,这样您可以更清楚地看到问题:
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
}
您传递给 AuthData.addItem
的 data
(最终传递给 reducer)并不是每次都是一个新对象——它是一个单一的“全局”(模块级) ) 常数,你每次使用它时只需改变它。这就是导致你的状态发生变化的原因——因为每个在你的 state.items
数组中结束的对象都是对同一个对象的引用,所以你所做的每个突变(在 submitHandler
内)最终都会改变复制!
您可以轻松地在子组件中修复此问题,方法是不是每次都简单地改变同一个对象,而是重新创建一个新对象。我看不出你在做什么,所以干脆停止做吧!我会重写如下:
// 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
}
这样一来,state"item"数组中的每个对象都是唯一的,你可以派发新的而不影响旧的!
reducer 函数工作正常,除了先前添加到 item
数组 属性 的对象元素在添加新对象元素时会被覆盖。
例如,如果 state.item
包含 {number: 1}
并且我添加 {number: 2}
,它变成 [{number: 2},{number: 2}]
而不是 [{number: 1},{number: 2}]
。
reducer函数:
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 };
}
};
这个问题有解决办法吗?
谢谢。
父组件:
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;
子组件:
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;
问题完全出在你的子组件上,与你的reducer无关。这就是您传递数据的方式,这些数据成为您发送的操作中的 addItem
有效负载。
我已经将子组件(或者说是整个模块)的相关部分转载如下,这样您可以更清楚地看到问题:
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
}
您传递给 AuthData.addItem
的 data
(最终传递给 reducer)并不是每次都是一个新对象——它是一个单一的“全局”(模块级) ) 常数,你每次使用它时只需改变它。这就是导致你的状态发生变化的原因——因为每个在你的 state.items
数组中结束的对象都是对同一个对象的引用,所以你所做的每个突变(在 submitHandler
内)最终都会改变复制!
您可以轻松地在子组件中修复此问题,方法是不是每次都简单地改变同一个对象,而是重新创建一个新对象。我看不出你在做什么,所以干脆停止做吧!我会重写如下:
// 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
}
这样一来,state"item"数组中的每个对象都是唯一的,你可以派发新的而不影响旧的!