AddToCart 和 RemoveFromCart 操作都会更新num和qty,而 HandleClick 和 InputQuantity 操作都会更新qty。
我对这两行代码有疑问:
num: productsReducer(state.productsReducer.num, AddToCart)
qty: productsReducer(state.productsReducer.qty, AddToCart)
其中prop num或qty仅由AddToCart操作更新。
其他3个动作-RemoveFromCart,HandleClick,InputQuantity-不更新道具编号或数量。而且我不知道如何允许上述操作更新道具编号和数量。
如何编写productReducer或我的操作(使num和qty都由AddToCart和RemoveFromCart更新,而qty由HandleClick和InputQuantity更新)?
我需要一个高阶减速器来处理多个动作吗?请帮忙。
CartContainer
// Both AddToCart and RemoveFromCart actions update num and qty
// Both HandleClick and InputQuantity actions update qty
const mapStateToProps = state => ({
num: productsReducer(state.productsReducer.num, AddToCart),
qty: productsReducer(state.productsReducer.qty, AddToCart),
products: state.productsReducer.products
});
ProductsReducer
import * as actionTypes from '../actions/actions';
const initialState = {
products: [],
num: [],
qty: [],
totalPrice: 0,
status: ''
};
const productsReducer = (state = initialState, action) => {
let updatedNum = state.num;
let updatedQty = state.qty;
let index;
if (action.num !== undefined) {
index = updatedNum.indexOf(action.num);
}
switch (action.type) {
case actionTypes.FETCH_PRODUCTS_REQUEST:
return {
...state,
status: action.status
}
case actionTypes.FETCH_PRODUCTS_SUCCESS:
return {
...state,
status: action.status,
products: action.products
}
case actionTypes.ADD_TO_CART:
// if num array doesn't have that element, insert that element
if (!state.num.includes(action.num)) {
updatedNum = state.num.concat(action.num);
updatedQty = state.qty.concat(1);
return {
...state,
num: updatedNum,
qty: updatedQty
}
// if element is present in array already, increase qty of it by 1
} else {
updatedQty = state.qty.slice(0);
updatedQty[index] += 1;
return {
...state,
qty: updatedQty
}
}
// After updating store, use CalculatePrice action to calculate updated price.
this.calculateTotalPrice(updatedNum, updatedQty);
case actionTypes.REMOVE_FROM_CART:
// remove clicked item with splice, and update state
updatedNum.splice(index, 1);
updatedQty.splice(index, 1);
return {
...state,
num: updatedNum,
qty: updatedQty
}
case actionTypes.HANDLE_CLICK:
let event = action.eventType;
console.log(event);
if (event === 'plus') {
updatedQty[index] += 1;
} else if (event === 'minus') {
updatedQty[index] -= 1;
}
return {
...state,
qty: updatedQty
}
case actionTypes.INPUT_QUANTITY:
const regex = /^[0-9\b]+$/;
if (regex.test(action.event.target.value)) {
updatedQty[index] = Number(action.event.target.value);
return {
...state,
qty: updatedQty
}
}
case actionTypes.CALCULATE_PRICE:
let arr = [];
console.log('updatedNum', action.num);
action.num.map(num => {
let index = num - 1;
arr.push(action.qty[index] * state.products[index].price);
});
if (arr.length > 0) {
let total = arr.reduce((acc, currentVal) => acc + currentVal);
return {
totalPrice: total
}
}
return {
totalPrice: 0
}
default:
return state;
}
}
export default productsReducer;
答案 0 :(得分:1)
所以我给你两个答案,第一个主要是解释为什么只有AddToChart更新道具而不更新其他动作的原因,第二个主要是由您决定的一组建议
1)为什么只有AddToChart才能更新道具(redux状态)
请在下面找到Redux官方文档中的报价,该报价也适用于您
为什么我的组件不能重新渲染,或者mapStateToProps没有运行? 到目前为止,不小心直接更改或修改您的状态是 动作执行后组件不重新渲染的最常见原因 已被派遣
此引用的重要部分是
意外地直接更改或修改您的状态
让我们看看这对您有何作用
在您的initialState中,您具有以下值
const initialState = {
num: [],
qty: []
};
num和qty字段是数组。
在productReducers的开头,您还需要以下一行
let updatedNum = state.num;
let updatedQty = state.qty;
let index;
在进一步介绍之前,让我们回顾一下javascript中数组的特殊之处吗?
请在帖子https://www.dyn-web.com/javascript/arrays/value-vs-reference.php
中找到报价也许您已经注意到,如果您将一个数组分配给另一个 变量并修改该变量的数组元素,即原始数组 也被修改。也许您已将数组传递给函数 希望只修改数组的本地副本,但是您发现 原始数组也已修改。发生这种情况是因为数组 是JavaScript中的引用类型。
这意味着如果您将数组分配给变量或传递数组 一个函数,它是对原始数组的引用 复制或传递,而不是数组的值。
因此,基本上,您的updatedNum和updatedQty不是新数组,而是对原始数组的引用,因此,每当您对updatedNum和updatedQty进行突变时,它们所引用的原始数组也会被更新。
在我们的例子中,原始数组实际上是我们在状态中拥有的数组,因此基本上每当有更新时,原始状态实际上都会发生变化,并且如第一引号所示,
意外的突变可防止在执行某项操作后重新呈现 派遣
在ADD_TO_CART
case actionTypes.ADD_TO_CART:
...
updatedNum = state.num.concat(action.num);
updatedQty = state.qty.concat(1);
使用concat()方法。
为什么对您有用?
来自MDN https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat
concat()方法用于合并两个或多个数组。这个方法 不会更改现有数组,而是返回一个新数组。
因此不会发生意外突变,您将返回一个全新的数组
在REMOVE_FROM_CHART
方法中
case actionTypes.REMOVE_FROM_CART:
updatedNum.splice(index, 1);
updatedQty.splice(index, 1);
splice()方法被使用。
来自MDN https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice
splice()方法通过删除或替换现有元素和/或添加新元素来更改数组的内容。
所以有一个突变,因为您不对新数组进行返回,而是对引用到该状态的原始数组进行了突变
在HANDLE_CLICK
和UPDATE_QUANTITY
updatedQty[index] -= 1;
再次使用这种方法,您可以对数组进行突变,而不是返回全新的数组,因此您可以对引用到该状态的原始数组进行突变
在当前情况下,如果执行以下更新
let updatedNum = state.num.concat();
let updatedQty = state.qty.concat();
//I would suggest you to use spread operator
let updatedNum = [...state.num]
let updatedQty = [...state.qty]
您最初的大部分问题都会得到解决
2)如何编写productReducer或我的操作,使num和qty都被更新
对于这个问题,我无法为您提供具体的分步指南,因为如何根据您当前的需求和未来可能的改进来决定商店的结构
但是,我可以提出一些建议
首先,可能我会摆脱以下部分
let updatedNum = state.num;
let updatedQty = state.qty;
let index;
if (action.num !== undefined) {
index = updatedNum.indexOf(action.num);
}
对我来说,我通常在隔离范围内的动作处理程序中处理这种检查和赋值,因为当代码变大时,我很难一路向上看我在哪里声明这些变量并其次,您永远无法确定它们是否被突变,从而使调试变得更加困难。另外,将来,您可能需要为某些操作处理程序更新声明逻辑,例如updateNum。如果您有这种声明,则可以中断应用程序的某些部分而无需意识到
第二,我相信下面的用法有点反模式
// After updating store, use CalculatePrice action to calculate updated price.
this.calculateTotalPrice(updatedNum, updatedQty);
如果您打算在动作处理程序中触发此功能,则该功能将无效,因为在上述几行中,您已经返回了一条语句。
如果您打算在ADD_CHART
,REMOVE_FROM_CHART
您可以将计算逻辑放入这些动作处理程序中,进行计算并返回新状态
例如
...
const totalPrice = Some logic related to updatedNum and updateQty
return {
...state,
num: updatedNum,
qty: updatedQty,
totalPrice
}
我希望我的回答将帮助您设计一条更清晰的路径。始终可以通过几种不同的方式来创建动作,动作减少器等。
但是,在进行redux时要记住的最重要的事情是,避免使用可能会导致意外突变的方法或设计,如果您要调试和查找问题,将会使您的生活变得艰难。
因此,在尝试相对复杂的事情(例如高阶减速器等)之前,您可以专注于根本原因。
答案 1 :(得分:0)
您违反了关于减速器的规则。说的规则一定不能改变其输入状态参数。您在这里做什么:
const productsReducer = (state = initialState, action) => {
let updatedNum = state.num;
let updatedQty = state.qty;
let index;
,甚至是该productsReducer
的代码行。现在,该规则有点误导且有些错误,我可能会明白我的意思,但是请记住,我们切勿突变state
的输入参数。
例如,所有这些都是不好的:
export default (state, action) => {
state[0] = ‘Dan’
state.pop()
state.push()
state.name = ‘Dan’
state.age = 30;
};
如果化简函数内部有state
和=
。您违反了reducer规则,结果导致应用程序出现问题。
因此,我认为为了解决此处的错误,我们需要了解以下规则:
为什么我的组件不能重新渲染,或者mapStateToProps没有运行? 到目前为止,最常见的原因是组件在 行动已派出
事实是您可以整天更改state
参数,而看不到任何错误消息。您可以为对象添加属性,也可以更改对象的属性,所有这些state
参数都可以使用,而Redux绝对不会给您任何错误消息。
Redux文档说不改变状态参数的原因是,告诉初学者不要改变状态比告诉他们何时可以改变状态更容易。
需要明确的是,我们仍应遵循Redux官方文档,从未修改过state
参数。我们绝对可以,您当然可以,但是这就是为什么您应该遵循Redux文档所说的原因。
我想帮助您了解Redux的幕后知识,以便您了解应用程序中的错误并进行修复。
让我们看一下Redux库本身的源代码,特别是从162行左右开始的代码段。您可以在以下网址查看它:https://raw.githubusercontent.com/reduxjs/redux/master/src/combineReducers.js
其以let hasChanged = false
开头的行
let hasChanged = false
const nextState = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
研究以下代码块才能真正理解其含义:
意外的突变可防止在执行某项操作后重新呈现 派遣
上面的代码片段是Redux的一部分,它执行一个动作,并在它被分派后立即将其发送到应用程序内所有不同的reducer。
因此,无论何时我们调度动作,上面的Redux代码都将被执行。
我将逐步引导您完成此代码,并更好地了解您分派操作时会发生什么。
首先发生的是我们设置了hasChanged
变量并将其设置为等于false
。
然后,我们进入一个for
循环,该循环将遍历应用程序内所有不同的reducer。
在for
循环的主体中,有一个名为previousStateForKey
的变量
将为该变量分配减速器返回的最后一个状态值。
Reducer每次被称为第一个参数的时间将是它最后一次运行时返回的state
。
因此,previousStateForKey
是对先前的state
值的引用,该值经过迭代后返回。下一行是调用reducer的地方。
第一个参数是减速器上一次返回的state
previousStateForKey
,然后第二个参数是action
对象。
您的减速器将运行,然后最终打开一些新的state
值,并将其分配给nextStateForKey
。因此,我们有hasChanged
,previousStateForKey
和nextStateForKey
,这是我们新的state
值。
在您的reducer运行并将新的state
分配给nextStateForKey
之后,Redux将立即检查您的reducer是否返回了undefined值,这是在我们的reducer首次初始化时会发生的情况然后Redux在此处使用if
语句对此进行检查:
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
这是化简器的主要规则之一,化简器永远不会返回未定义的值。因此,Redux询问使用此if
语句时,它们是否返回未定义的?如果是这样,则抛出此错误消息。
假设您通过了该检查,那么在以下代码行中,事情变得更加有趣:
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
hasChanged
将获得nextStateForKey
与您的previousStateForKey
之间直接比较的价值。
因此,不更改您的state
参数的规则归结于上面的代码行。
该比较的目的是检查nextStateForKey
和previousStateForKey
是否与内存中的数组或对象完全相同。
因此,如果您只是返回了上次reducer运行时的某个数组及其与内存完全相同的数组,则hasChanged
将为false值,否则,如果您的reducer返回了在其中创建的全新数组您的reducer和上次运行reducer时,数组与数组完全不同,那么hasChanged
将等于true。
因此,这个hasChanged
变量是说该减速器返回的任何状态是否已更改?
因此,for
循环将遍历所有减速器,考虑到您传递给这些减速器的所有不同减速器,hasChanged
将是true
或false
它。
注意只有一个hasChanged
变量吗?没有每个减速器一个。如果任何化简器已更改值或为整数,数字或字符串返回了不同的值,数组或对象或其他值,则hasChanged
将等于true
。
在for
循环之后,事情仍然变得更加有趣。我们查看hasChanged
的值,如果将其更改为true,则整个函数的结果将返回新的state
对象。由您所有不同的化简器组装的整个新state
对象,否则,如果hasChanged
等于false
,我们将返回state
并且state
是一个引用到减速器上次运行时返回的所有state
。
总而言之,Redux中的那段代码检查运行减速器后是否发现任何减速器是否返回了全新的数组或对象,数字或字符串(如果这样做),则Redux将返回全新的结果从所有减速器中返回,否则,如果减速器未返回新值,它将从减速器中返回旧状态。
这里有什么意义?
与您的问题相关的原因是,如果Redux返回旧状态值,则Redux将不会通知您的应用程序其余部分数据已更改。
如果您确实有一个新的state
,如果您从其中一个化简器中突变了state
并返回了nextState
,则Redux会查看该对象并说哦,我们有一些所有这些不同的reducer都处于新状态,它将通知您的其余应用程序,包括React应用程序,您有可用的新状态,这将导致React应用程序重新呈现。
总而言之,您不得在化简器中更改状态参数的原因不是因为您无法对其进行突变,原因是,如果您不小心返回了抽入化简器中的相同值,如果您说{ {1}}在您的化简器中,并且它仍然是相同的对象或数组,是否已修改,Redux在这里将说没有区别,它在内存中是相同的对象或数组,没有任何变化,因此我们没有对数据进行任何更新在应用程序和React应用程序内部不需要渲染自身,这就是为什么您不会在屏幕上看到任何更新的计数器的原因。那就是这里发生的事情。
这就是为什么Redux文档告诉您不要在化简器中突变return state;
参数。如果返回state
自变量,则是否进行更改;如果以state
的形式返回相同的值,则不会对应用程序进行更改。
这就是Redux文档试图使用该规则传达的内容,请不要更改return state;
参数。
告诉您不要像在Redux文档中那样更改state
参数,要比我刚才给您的答案要容易得多。